Erstellen einer ultraschnellen Nahrungssuche-API mit Go und Redis
Nutzen Sie die Leistung von Go und Redis-Caching, um eine Such-API zu erstellen, die Ergebnisse in unter 10 ms für eine Datenbank mit Millionen von Lebensmitteln liefert.
Auf dieser Seite
In der Welt der Gesundheits- und Wellness-Apps sind Daten das A und O. Benutzer erwarten sofortigen Zugriff auf Nährwertinformationen für Millionen von Lebensmitteln. Eine langsame, umständliche Suche ist ein K.-o.-Kriterium. Wenn Ihre API Sekunden braucht, um zu antworten, haben Sie bereits verloren. Die Herausforderung ist klar: Wie fragen Sie einen riesigen Datensatz ab und liefern Ergebnisse im Handumdrehen?
Heute werden wir dieses Problem direkt angehen. Wir werden eine blitzschnelle Such-API für Ernährungsdaten erstellen, unter Verwendung der rohen Leistung von Go und der unglaublichen Geschwindigkeit von Redis. Wir verwenden Redis nicht nur als einfachen Key-Value-Cache; wir nutzen das Modul RediSearch, um eine leistungsstarke, indizierte Suchmaschine zu schaffen, die Ergebnisse aus Millionen von JSON-Dokumenten mit Latenzzeiten unter 10 ms liefern kann.
Wir werden eine REST-API erstellen, die es Benutzern ermöglicht, Volltextsuchen nach Lebensmitteln durchzuführen. Wir werden sehen, wie wir unsere Daten strukturieren, effizient indizieren und über eine saubere Go-API bereitstellen.
Voraussetzungen:
- Go (v1.18+): Ein solides Verständnis der Go-Grundlagen ist erforderlich.
- Docker: Der einfachste Weg, um Redis mit den erforderlichen Modulen auszuführen.
- Ein REST-Client: Tools wie
curl, Postman oder Insomnia zum Testen unserer API.
Warum dies für Entwickler wichtig ist: Dies ist keine rein theoretische Übung. Die Architektur, die wir erstellen werden, ist ein leistungsstarkes Muster für jede Anwendung, die Hochgeschwindigkeitssuche über große, strukturierte Datensätze erfordert – Produktkataloge, Benutzerverzeichnisse, Dokumentenrepositorys und mehr. Sie werden praktische Fähigkeiten in der Microservice-Optimierung, erweiterter Redis-Nutzung und dem Erstellen von Hochdurchsatz-Backend-Systemen erwerben.
Das Problem verstehen
Ein traditioneller Ansatz könnte eine relationale Datenbank (wie PostgreSQL) mit einer LIKE-Abfrage beinhalten. Für ein paar tausend Datensätze funktioniert das. Für Millionen? Das wird zum Stillstand kommen. Sie könnten Volltextsuchfunktionen zu Postgres hinzufügen, aber das erhöht die Komplexität.
Eine weitere häufige Lösung ist die Verwendung einer dedizierten Suchmaschine wie Elasticsearch. Obwohl unglaublich leistungsstark, ist es auch ein komplexes Infrastrukturelement zu verwalten.
Unser Ansatz findet einen optimalen Punkt. Wir verwenden Redis, ein Tool, das viele Entwickler bereits kennen und für Caching schätzen, aber wir erschließen seine Suchfähigkeiten. Durch die Verwendung des RediSearch-Moduls erhalten wir die Leistung einer dedizierten Suchmaschine mit der Einfachheit und dem geringen Overhead von Redis. Wir speichern unsere Daten im Speicher, indiziert für blitzschnelle Lookups, was es perfekt für diese Art von leseintensiver Workload macht.
Hochleistungs-Sucharchitektur
Das folgende Diagramm zeigt unsere Redis-gestützte Search-API-Architektur:
Voraussetzungen & Einrichtung
Lassen Sie uns unsere Umgebung vorbereiten.
Redis Stack mit Docker ausführen
Redis Stack enthält das RediSearch-Modul, das wir benötigen. Es ist der einfachste Weg zu beginnen.
docker run -d --name redis-stack -p 6379:6379 -p 8001:8001 redis/redis-stack:latest
Dieser Befehl startet einen Redis-Container mit RediSearch und exponiert den Redis-Port (6379) und den RedisInsight GUI-Port (8001). Sie können jetzt in Ihrem Browser eine Verbindung zu localhost:8001 herstellen, um einen visuellen Blick auf Ihre Daten zu erhalten.
2. Richten Sie Ihr Go-Projekt ein
Lassen Sie uns unser Projektverzeichnis erstellen und ein Go-Modul initialisieren.
mkdir go-redis-search
cd go-redis-search
go mod init github.com/ihr-benutzername/go-redis-search
3. Go-Abhängigkeiten installieren
Wir werden den offiziellen go-redis-Client verwenden, der ausgezeichnete Unterstützung für Redis-Module bietet, und Gin als leichtgewichtigen HTTP-Router.
go get github.com/redis/go-redis/v9
go get github.com/gin-gonic/gin
Unsere Einrichtung ist abgeschlossen! Lassen Sie uns mit dem Aufbau beginnen. ✨
Modelldaten für Lebensmittel und Seed für Redis
Zuerst brauchen wir Daten. Wir definieren eine FoodItem-Struktur in Go und schreiben dann ein Skript, um einen großen Datensatz zu generieren und in Redis zu laden.
Was wir tun
Wir speichern unsere Lebensmitteldaten als JSON-Dokumente in Redis. JSON ist flexibel und wird von RediSearch gut unterstützt. Jedes Lebensmittel wird einen eindeutigen Schlüssel wie food:1, food:2 usw. haben.
Eingabe: Go-Struktur FoodItem mit Feldern Name, Marke, Kalorien, Protein, Fett, Kohlenhydrate
Ausgabe: 1 Million JSON-Dokumente, gespeichert in Redis mit Schlüsseln food:1 bis food:1000000
Implementierung
Erstellen Sie eine Datei namens main.go:
// main.go
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"math/rand"
"time"
"github.com/gin-gonic/gin"
"github.com/redis/go-redis/v9"
)
var (
ctx = context.Background()
rdb *redis.Client
)
// FoodItem represents the structure of our nutrition data
type FoodItem struct {
Name string `json:"name"`
Brand string `json:"brand"`
Calories float64 `json:"calories"`
Protein float64 `json:"protein"`
Fat float64 `json:"fat"`
Carbs float64 `json:"carbs"`
}
func main() {
// Connect to Redis
rdb = redis.NewClient(&redis.Options{
Addr: "localhost:6379",
})
_, err := rdb.Ping(ctx).Result()
if err != nil {
log.Fatalf("Could not connect to Redis: %v", err)
}
log.Println("Connected to Redis!")
// Seed data and create search index
seedDataAndCreateIndex()
// Setup Gin router and API endpoints (we'll add this later)
// ...
}
func seedDataAndCreateIndex() {
// Check if data is already seeded
count, err := rdb.Exists(ctx, "food:1").Result()
if err != nil {
log.Fatalf("Error checking for existing data: %v", err)
}
if count > 0 {
log.Println("Data already seeded. Skipping seeding.")
return
}
log.Println("Seeding data... (this might take a moment)")
// Sample data for generation
brands := []string{"HealthyCo", "NutriFoods", "FitBites", "Organics"}
names := []string{"Yogurt", "Chicken Breast", "Almonds", "Oats", "Apple"}
totalItems := 1_000_000 // Let's create a million items!
// Use a pipeline for mass insertion for better performance
pipe := rdb.Pipeline()
for i := 1; i <= totalItems; i++ {
item := FoodItem{
Name: fmt.Sprintf("%s %s", brands[rand.Intn(len(brands))], names[rand.Intn(len(names))]),
Brand: brands[rand.Intn(len(brands))],
Calories: float64(rand.Intn(500) + 50),
Protein: float64(rand.Intn(50)),
Fat: float64(rand.Intn(30)),
Carbs: float64(rand.Intn(100)),
}
// Marshal the struct to JSON
jsonBytes, _ := json.Marshal(item)
key := fmt.Sprintf("food:%d", i)
// Add the JSON.SET command to the pipeline
pipe.JSONSet(ctx, key, "$", string(jsonBytes))
}
// Execute the pipeline
_, err = pipe.Exec(ctx)
if err != nil {
log.Fatalf("Failed to seed data: %v", err)
}
log.Printf("Successfully seeded %d items.\
", totalItems)
// Create RediSearch Index (more on this in the next step)
}
Wie es funktioniert
- Wir definieren unsere
FoodItem-Struktur mit JSON-Tags für die Serialisierung. - In
seedDataAndCreateIndexprüfen wir zuerst, ob die Daten existieren, um ein erneutes Seeding bei jedem Start der App zu vermeiden. - Wir verwenden eine
pipeline, um Tausende vonJSON.SET-Befehlen in einer einzigen Runde zum Server zu senden. Dies ist dramatisch schneller als das Senden eines Befehls nach dem anderen.
Daten mit dem RediSearch-Modul indizieren
Jetzt, da unsere JSON-Daten in Redis sind, müssen wir sie durchsuchbar machen. Wir erstellen einen Suchindex, der RediSearch mitteilt, auf welche Felder es achten soll.
Was wir tun
Wir verwenden den FT.CREATE-Befehl, um ein Schema für unseren Index zu definieren. Wir indizieren die Felder name und brand als TEXT für die Volltextsuche und die numerischen Felder für mögliche Bereichsabfragen.
Eingabe: JSON-Dokumente, gespeichert unter den Schlüsseln food:*
Ausgabe: Suchindex idx:foods mit indizierten Feldern zu Name, Marke und Nährwerten
Implementierung
Fügen Sie den folgenden Code am Ende der Funktion seedDataAndCreateIndex in main.go hinzu:
// main.go (inside seedDataAndCreateIndex function)
// ... after seeding data
log.Println("Creating search index...")
// Index schema definition
schema := redis.NewSchema().
AddField(redis.NewTextFieldOptions("$.name", redis.TextFieldOptions{Weight: 5.0, As: "name"})).
AddField(redis.NewTextFieldOptions("$.brand", redis.TextFieldOptions{As: "brand"})).
AddField(redis.NewNumericFieldOptions("$.calories", redis.NumericFieldOptions{As: "calories"})).
AddField(redis.NewNumericFieldOptions("$.protein", redis.NumericFieldOptions{As: "protein"})).
AddField(redis.NewNumericFieldOptions("$.fat", redis.NumericFieldOptions{As: "fat"})).
AddField(redis.NewNumericFieldOptions("$.carbs", redis.NumericFieldOptions{As: "carbs"}))
// Create the index
err = rdb.FTCreate(ctx, "idx:foods", &redis.FTCreateOptions{
Prefix: []string{"food:"},
Schema: schema,
}).Err()
// We ignore the "Index already exists" error
if err != nil && err.Error() != "Index already exists" {
log.Fatalf("Failed to create index: %v", err)
}
if err == nil {
log.Println("Search index created successfully.")
} else {
log.Println("Search index already exists.")
}
Wie es funktioniert
FTCreate: Dieser Befehl erstellt einen neuen Suchindex namensidx:foods.Prefix: Wir teilen dem Index mit, nur Schlüssel zu berücksichtigen, die mitfood:beginnen. Dies isoliert unsere Lebensmitteldaten.Schema: Hier definieren wir die zu indizierenden Felder.$.nameund$.brandsind JSONPath-Ausdrücke, die auf die Felder in unseren JSON-Dokumenten zeigen. Wir indizieren sie alsTEXT. Wir geben dem Feldnameauch ein höheresWeight, damit Übereinstimmungen im Namen in den Suchergebnissen relevanter erscheinen.- Wir indizieren auch die numerischen Felder, was es uns ermöglichen würde, Abfragen wie „alle Lebensmittel mit weniger als 100 Kalorien finden" auszuführen.
Führen Sie Ihre Anwendung jetzt einmal aus, um die Daten zu seeden und den Index zu erstellen.
go run main.go
# Output should be:
# Connected to Redis!
# Seeding data... (this might take a moment)
# Successfully seeded 1000000 items.
# Creating search index...
# Search index created successfully.
Search-API-Endpunkt mit dem Gin-Framework erstellen
Mit unseren indizierten Daten können wir nun die API zum Abfragen erstellen.
Was wir tun
Wir verwenden das Gin-Web-Framework, um einen einfachen /search-Endpunkt zu erstellen, der einen Query-Parameter q akzeptiert. Dieser Endpunkt wird RediSearch verwenden, um übereinstimmende Lebensmittel zu finden.
Eingabe: HTTP-GET-Anfrage mit Query-Parameter q (z.B. /search?q=yogurt)
Ausgabe: JSON-Array von bis zu 10 übereinstimmenden FoodItem-Objekten mit Name, Marke und Nährwertdaten
Implementierung
Aktualisieren Sie Ihre main-Funktion und fügen Sie eine neue Handler-Funktion hinzu.
// main.go
// ... (keep the existing code)
func main() {
// Connect to Redis
rdb = redis.NewClient(&redis.Options{
Addr: "localhost:6379",
})
_, err := rdb.Ping(ctx).Result()
if err != nil {
log.Fatalf("Could not connect to Redis: %v", err)
}
log.Println("Connected to Redis!")
// Seed data and create search index
seedDataAndCreateIndex()
// Setup Gin router
router := gin.Default()
router.GET("/search", searchHandler)
log.Println("Starting server on :8080")
router.Run(":8080")
}
func searchHandler(c *gin.Context) {
query := c.Query("q")
if query == "" {
c.JSON(400, gin.H{"error": "Query parameter 'q' is required"})
return
}
// Sanitize and format the query for RediSearch
// For example, simple fuzzy search on name and brand
searchQuery := fmt.Sprintf("@name|brand:%s*", query)
// Perform the search
docs, _, err := rdb.FTSeach(ctx, "idx:foods", searchQuery, &redis.FTSearchOptions{
Limit: &redis.Limit{
Num: 10, // Limit to 10 results
},
}).Result()
if err != nil {
c.JSON(500, gin.H{"error": "Failed to perform search"})
return
}
// We get back a list of documents. The first element is the total count,
// and the rest are key-value pairs.
var results []FoodItem
for i := 1; i < len(docs); i++ {
// Each document is a key followed by its fields
doc, ok := docs[i].(redis.Document)
if !ok {
continue
}
// The document properties contains the JSON string
var item FoodItem
err := json.Unmarshal([]byte(doc.Properties["$"].(string)), &item)
if err == nil {
results = append(results, item)
}
}
c.JSON(200, results)
}
Wie es funktioniert
- Wir richten einen Gin-Router mit einem GET-Endpunkt bei
/searchein. searchHandlerholt den Query-Parameterq.@name|brand:%s*: Dies ist die RediSearch-Abfragesyntax. Es bedeutet „Dokumente finden, bei denen das FeldnameODERbrandden Abfragetext enthält". Das*aktiviert Präfixsuche, sodass die Suche nach „chick" „Chicken" findet.rdb.FTSearch: Dies ist der wichtige Funktionsaufruf. Wir übergeben unseren Indexnamen und die Abfrage.- Wir begrenzen die Ergebnisse auf 10 aus Leistungsgründen.
- Das Ergebnis von
FTSearchenthält den Dokumentschlüssel und die vollständige JSON-Nutzlast (unter der Eigenschaft$). Wir iterieren durch die Ergebnisse, unmarshallen das JSON zurück in unsereFoodItem-Struktur und bauen unsere Antwort auf.
Führen Sie die App erneut aus (go run main.go) und testen Sie sie!
# In another terminal
curl "http://localhost:8080/search?q=healthy%20yogurt"
Sie sollten ein JSON-Array mit übereinstimmenden Lebensmitteln nahezu sofort zurückerhalten!
Leistungsüberlegungen & Caching
Unsere API ist bereits schnell, aber wir können sie für wiederholte Abfragen noch schneller machen. Hier kommt ein klassisches Cache-Aside-Muster ins Spiel.
Die Logik ist einfach:
- Bevor wir RediSearch aufrufen, prüfen wir, ob das Ergebnis in einem einfachen Redis-Schlüssel existiert (z.B.
cache:healthy yogurt). - Wenn es existiert (Cache-Treffer), geben wir die zwischengespeicherten Daten sofort zurück.
- Wenn es nicht existiert (Cache-Fehlschlag), fragen wir RediSearch ab, speichern das Ergebnis im Cache-Schlüssel mit einer Ablaufzeit (TTL) und geben es dann zurück.
Hier ist, wie Sie searchHandler ändern können, um dies zu implementieren:
// main.go (updated searchHandler)
func searchHandler(c *gin.Context) {
query := c.Query("q")
if query == "" {
c.JSON(400, gin.H{"error": "Query parameter 'q' is required"})
return
}
cacheKey := "cache:" + query
// 1. Check the cache first
cachedResult, err := rdb.Get(ctx, cacheKey).Result()
if err == nil {
// Cache hit!
var results []FoodItem
json.Unmarshal([]byte(cachedResult), &results)
c.JSON(200, results)
return
}
// 2. Cache miss. Query RediSearch
searchQuery := fmt.Sprintf("@name|brand:%s*", query)
docs, _, err := rdb.FTSearch(ctx, "idx:foods", searchQuery, &redis.FTSearchOptions{
Limit: &redis.Limit{Num: 10},
}).Result()
if err != nil {
c.JSON(500, gin.H{"error": "Failed to perform search"})
return
}
var results []FoodItem
// ... (same parsing logic as before)
for i := 1; i < len(docs); i++ {
doc, ok := docs[i].(redis.Document)
if !ok { continue }
var item FoodItem
err := json.Unmarshal([]byte(doc.Properties["$"].(string)), &item)
if err == nil {
results = append(results, item)
}
}
// 3. Store result in cache with a TTL
jsonBytes, _ := json.Marshal(results)
err = rdb.Set(ctx, cacheKey, jsonBytes, 5*time.Minute).Err()
if err != nil {
// Log the error but don't fail the request
log.Printf("Failed to cache result: %v", err)
}
c.JSON(200, results)
}
Benchmarking
Lassen Sie uns beweisen, dass es schnell ist. Mit einem Tool wie wrk:
# -t = threads, -c = connections, -d = duration
wrk -t8 -c100 -d30s "http://localhost:8080/search?q=yogurt"
Das erste Mal, wenn Sie dies ausführen, werden Sie eine sehr niedrige Latenz sehen. Aber das zweite Mal, wenn alle Ergebnisse zwischengespeichert sind, wird der Durchsatz noch höher und die Latenzen noch niedriger sein, wahrscheinlich deutlich unter 10 ms. Dies demonstriert die Kraft der Cache-Aside-Strategie.
Alternative Ansätze
- Elasticsearch/OpenSearch: Dies sind extrem leistungsstarke, funktionsreiche Suchmaschinen. Sie sind eine großartige Wahl für komplexe Suchanforderungen (z.B. Aggregationen, komplexe Filterung, Relevanz-Tuning). Allerdings bringen sie einen höheren betrieblichen Aufwand und Ressourcenverbrauch im Vergleich zu Redis mit.
- Datenbank-Volltextsuche (PostgreSQL, MySQL): Die meisten modernen Datenbanken haben eingebaute FTS-Fähigkeiten. Dies kann eine gute Option sein, wenn Sie Ihren Stack einfach halten möchten. Die Leistung könnte für sehr große Datensätze oder Hochdurchsatz-Szenarien möglicherweise nicht an eine In-Memory-Lösung wie Redis heranreichen.
Fazit
Wir haben erfolgreich eine Hochleistungs-Such-API erstellt, die Millionen von Datensätzen mit unglaublich niedriger Latenz verarbeiten kann. Durch die Kombination der Effizienz von Go mit Redis und dem RediSearch-Modul haben wir eine Lösung geschaffen, die sowohl leistungsstark als auch relativ einfach zu verwalten ist.
Leistungsauswirkung: Redis RediSearch liefert Abfrage-Latenzzeiten unter 10 ms für Volltextsuchen über Millionen von Dokumenten. Gora Goroutinen ermöglichen über 100.000 gleichzeitige Anfragen mit minimalem Ressourcen-Overhead. In-Memory-Indizierung bietet 10-100x schnellere Lookups im Vergleich zu traditionellen Datenbank-LIKE-Abfragen. In Kombination mit Cache-Aside-Mustern erreicht diese Architektur Latenzzeiten im 99. Perzentil unter 5 ms für zwischengespeicherte Abfragen.
Sie haben jetzt ein robustes Muster für jede Anwendung, die schnelle Suche benötigt. Sie können dies erweitern, indem Sie komplexere Abfragen, Paginierung und Filterung nach numerischen Feldern hinzufügen. Die Grundlage ist solide.
Nächste Schritte
- Paginierung für die Suchergebnisse implementieren.
- Filterung nach
Kalorien,Proteinusw. hinzufügen. - Fortgeschrittenere RediSearch-Funktionen wie Fuzzy-Matching und Geo-Filterung erkunden.
Für weitere Backend-Optimierungsmuster erkunden Sie Erstellen von HIPAA-konformen Datenpipelines mit FastAPI oder Ereignisgesteuerte Workout-Verarbeitung mit Node.js und RabbitMQ. Für Microservices-Architekturmuster schauen Sie sich Skalierung einer Fitness-App von 1k auf 1M Benutzer mit Kubernetes an.
Ressourcen
- Go-Redis-Client: https://github.com/redis/go-redis
- RediSearch-Dokumentation: https://redis.io/docs/stack/search/
- Gin Web Framework: https://github.com/gin-gonic/gin
Kurzübersicht
| Aspekt | Wichtige Informationen |
|---|---|
| Thema | Entwicklung |
| Relevanz | Patientenaufklärung und -bewusstsein |
| Maßnahme | Besprechen Sie dies mit Ihrem Arzt |
| Häufigkeit | Nach Empfehlung Ihres Behandlungsteams |
| Evidenz | Basierend auf aktuellen klinischen Leitlinien |
Häufig gestellte Fragen
F: Woher weiß ich, ob dies auf mich zutrifft? A: Besprechen Sie Ihre persönliche und familiäre Krankengeschichte mit Ihrem Arzt, um Ihre individuellen Risikofaktoren zu bewerten und geeignete nächste Schritte zu bestimmen.
F: Was sollte ich meinen Arzt fragen? A: Gute Fragen sind: „Was bedeuten meine Ergebnisse?", „Was sind meine Optionen?", „Welche Lebensstiländerungen würden helfen?" und „Wann sollte ich nachuntersuchen lassen?"
F: Gibt es Lebensstiländerungen, die helfen können? A: Die meisten Erkrankungen profitieren von grundlegenden Maßnahmen: ausgewogene Ernährung, regelmäßige körperliche Aktivität (über 150 Minuten/Woche), erholsamer Schlaf (7-9 Stunden), Stressbewältigung und Vermeidung von Tabak.
Medizinischer Haftungsausschluss
Dieser Artikel dient ausschließlich zu Informationszwecken und stellt keine medizinische Beratung dar. Konsultieren Sie immer einen qualifizierten Arzt, bevor Sie Entscheidungen über Ihre Gesundheit, Behandlung oder Pflege treffen. Individuelle Umstände variieren, und was für eine Person angemessen ist, ist möglicherweise nicht für eine andere geeignet.
Wenn Sie einen medizinischen Notfall haben, rufen Sie sofort 911 (oder Ihre örtliche Notrufnummer) an.