Einführung in Feature Stores
Die Notwendigkeit eines Feature Stores wird oft erst durch praktische Erfahrungen deutlich. Ein Betrugsmodell funktioniert in einer Testumgebung, versagt jedoch in der Produktion. Ein Supportmitarbeiter gibt eine allgemeine Antwort, weil er keine Informationen über den Benutzer hat. Ein Empfehlungssystem berechnet denselben „30-Tage-Ausgaben“-Wert in drei verschiedenen Jobs, wobei zwei von ihnen unterschiedliche Ergebnisse liefern.
Ein Feature Store ist die Infrastruktur, die diese Probleme löst. Er definiert Merkmale einmal, speichert sie in zwei Formaten (eines für das Training und eines für die Bereitstellung) und hält beide synchron. In diesem Artikel wird ein minimaler Feature Store von Grund auf in Python entwickelt, unter Verwendung von DuckDB, Parquet, Redis und FastAPI. Anschließend wird untersucht, wie KI-Anwendungen die Nutzung solcher Systeme beeinflussen.
Probleme, die ein Feature Store löst
Das klassische Problem ist die Diskrepanz zwischen Training und Bereitstellung: Der SQL-Code, der das Trainingsset erstellt, ist nicht derselbe Code, der bei der Inferenz verwendet wird, was zu abweichenden Werten führt. Dieses Problem ist real, und die Trennung zwischen Offline- und Online-Daten ist die gängige Lösung.
Das moderne Problem ist umfassender. Große Sprachmodelle (LLMs) und retrieval-augmented generation (RAG)-Pipelines benötigen strukturierte Benutzerkontexte in weniger als 10 Millisekunden bei jeder Anfrage. Ein LLM hat kein Gedächtnis darüber, wer der Benutzer ist. Um personalisierte Ausgaben zu erhalten, müssen wir den Tarif des Benutzers, die letzten Aktivitäten und den Kontostatus in den Prompt einfügen. Ein Feature Store bietet genau die Online-Speicher- und Abruf-API, die dafür erforderlich ist.
Die fünf Komponenten eines Feature Stores
- Ein Feature-Register, das Merkmale als Code definiert.
- Ein Offline-Speicher auf Parquet, der mit DuckDB abgefragt wird, für Training und Backfills.
- Ein Online-Speicher auf Redis für latenzarme Abfragen bei der Inferenz.
- Eine Materialisierungspipeline, die die neuesten Werte vom Offline- in den Online-Speicher überträgt.
- Ein FastAPI-Dienst, der eine typisierte Abruf-API bereitstellt.
Beispiel: Ein personalisierter LLM-Empfehlungsdienst
In diesem Beispiel betreiben wir einen Streaming-Dienst. Wenn ein Benutzer die App öffnet, generiert ein LLM eine kurze, personalisierte Nachricht mit der Empfehlung „Was als Nächstes ansehen“. Das LLM benötigt drei Informationen über den Benutzer:
- Feature: user_segment (Typ: string, Aktualisierung: täglich)
- Feature: watch_count_30d (Typ: int, Aktualisierung: stündlich)
- Feature: last_genre (Typ: string, Aktualisierung: bei jedem Ereignis)
Die Entität ist user_id. Diese drei Merkmale werden registriert, materialisiert und dem LLM zur Verfügung gestellt.
1. Definition des Feature-Registers
Ein Register ist ein Ort, an dem Merkmale einmal deklariert werden, zusammen mit ihrer Entität, Datentyp und Quelle. Hier verwenden wir eine Dataclass.
from dataclasses import dataclass
from typing import Literal
@dataclass(frozen=True)
class Feature:
name: str
entity: str
dtype: Literal["int", "float", "str"]
source: str # Pfad zu einer Parquet-Datei oder einer SQL-Ansicht
REGISTRY: dict[str, Feature] = {
"user_segment": Feature("user_segment", "user_id", "str", "data/user_segment.parquet"),
"watch_count_30d": Feature("watch_count_30d", "user_id", "int", "data/watch_count_30d.parquet"),
"last_genre": Feature("last_genre", "user_id", "str", "data/last_genre.parquet"),
}
Wenn Sie es ausführen, zeigt die Ausgabe:
Registrierte Merkmale:
user_segment entity=user_id dtype=str source=data/user_segment.parquet
watch_count_30d entity=user_id dtype=int source=data/watch_count_30d.parquet
last_genre entity=user_id dtype=str source=data/last_genre.parquet
Dies ist der Vertrag. Jedes andere Modul liest aus REGISTRY, sodass das Umbenennen eines Merkmals, das Ändern seines Datentyps oder das Verweisen auf eine neue Quelle an einem Ort erfolgt. In Produktionssystemen wäre dies YAML oder ein Python-Modul, das in ein Git-Repository eingecheckt wird, mit Code-Überprüfung bei jeder Änderung.
2. Aufbau des Offline-Speichers mit DuckDB und Parquet
Der Offline-Speicher enthält die gesamte Historie jedes Merkmals. Parquet-Dateien dienen als Speicherschicht, während DuckDB als Abfrage-Engine fungiert. DuckDB kann Parquet-Dateien direkt lesen, was bedeutet, dass kein separates Datenbankmanagement erforderlich ist.
import duckdb
import pandas as pd
def get_historical_features(
entity_df: pd.DataFrame, features: list[str]
) -> pd.DataFrame:
con = duckdb.connect()
con.register("entities", entity_df)
base = "SELECT * FROM entities"
for fname in features:
f = REGISTRY[fname]
src = f.source.replace("'", "''")
con.execute(f"CREATE VIEW {fname}_src AS SELECT * FROM '{src}'")
base = f"
SELECT t.*, s.{fname}
FROM ({base}) t
ASOF LEFT JOIN {fname}_src s
ON t.user_id = s.user_id
AND t.event_timestamp >= s.event_timestamp
"
return con.execute(base).df()
Wenn Sie es ausführen, zeigt die Ausgabe:
user_id
event_timestamp
user_segment
watch_count_30d
last_genre
8a2f
2026-05-05 12:00:00
casual
22
NaN
b13c
2026-05-07 20:00:00
casual
5
thriller
8a2f
2026-05-07 22:00:00
power_user
47
documentary
Der AsOf-Join ist der zeitpunktbezogene Join. Für jede Entitätszeile wird der aktuellste Merkmalswert ausgewählt, dessen Zeitstempel vor oder zum Zeitpunkt des Ereignisses liegt. Dies verhindert Datenlecks, bei denen eine Trainingszeile mit einem Merkmalswert erstellt wird, der zum Zeitpunkt der Vorhersage noch nicht existierte.
3. Einrichtung des Online-Speichers auf Redis
Der Online-Speicher hält nur den neuesten Wert pro Entität. Redis ist die gängige Wahl, da Hash-Abfragen sub-millisekundenschnell sind.
import json
import fakeredis # Verwenden Sie redis.Redis() gegen einen echten Server in der Produktion
r = fakeredis.FakeRedis(decode_responses=True)
def write_online(entity: str, entity_id: str, values: dict) -> None:
r.hset(
f"{entity}:{entity_id}",
mapping={k: json.dumps(v) for k, v in values.items()},
)
def read_online(entity: str, entity_id: str, features: list[str]) -> dict:
raw = r.hmget(f"{entity}:{entity_id}", features)
return {f: json.loads(v) if v else None for f, v in zip(features, raw)}
Wenn Sie es ausführen, zeigt die Ausgabe:
read_online -> {'user_segment': 'power_user', 'watch_count_30d': 47, 'last_genre': 'documentary'}
missing key -> {'user_segment': None}
Die Schlüsselstruktur ist entity:entity_id. Der Wert ist ein Hash mit einem Feld pro Merkmal. Ein einzelnes HMGET gibt alle angeforderten Merkmale in einem einzigen Abruf zurück. Auf einer lokalen Redis-Instanz mit drei Merkmalen wird dies in weniger als 1 ms abgeschlossen.
4. Ausführung der Materialisierungspipeline
Die Materialisierung überträgt Werte vom Offline- in den Online-Speicher. In einem realen System läuft dies nach einem Zeitplan (Airflow, Cron, ein Streaming-Job). Hier ist es eine Funktion.
def materialize(features: list[str]) -> None:
by_entity: dict[str, dict] = {}
for fname in features:
f = REGISTRY[fname]
src = f.source.replace("'", "''")
df = duckdb.sql(f"
SELECT {f.entity}, {fname}
FROM '{src}'
QUALIFY ROW_NUMBER() OVER (
PARTITION BY {f.entity}
ORDER BY event_timestamp DESC
) = 1
").df()
for _, row in df.iterrows():
by_entity.setdefault(row[f.entity], {})[fname] = row[fname]
for entity_id, values in by_entity.items():
write_online("user_id", entity_id, values)
Wenn Sie es ausführen, zeigt die Ausgabe:
user_id:8a2f -> {'user_segment': 'power_user', 'watch_count_30d': 47, 'last_genre': 'documentary'}
user_id:b13c -> {'user_segment': 'casual', 'watch_count_30d': 5, 'last_genre': 'thriller'}
Die QUALIFY-Klausel hält die neueste Zeile pro Entität. Wir gruppieren alle Merkmale für denselben Benutzer in einem Redis-Schreibvorgang, um die Anzahl der Abrufe zu minimieren. Diese Funktion sollte in dem Rhythmus ausgeführt werden, den jedes Merkmal benötigt: stündlich für watch_count_30d, nahezu in Echtzeit für last_genre, täglich für user_segment. Das Register ist der richtige Ort, um diesen Rhythmus in einer realen Implementierung zu kodifizieren.
5. Bereitstellung des FastAPI-Abrufdienstes
Der Abrufdienst ist die Produktionsoberfläche, die von der LLM-Anwendung aufgerufen wird.
f = resp.json()["features"]
print("\nPrompt, den das LLM erhalten würde:")
print(
f" System: Sie empfehlen Shows für einen Streaming-Dienst.\n"
f" Benutzerkontext: segment={f['user_segment']}, "
f"gesehen {f['watch_count_30d']} Titel in den letzten 30 Tagen, "
f"letztes Genre: {f['last_genre']}.
"
f" Aufgabe: schlagen Sie 3 Titel in einer freundlichen, kurzen Nachricht vor."
)
Wenn Sie es ausführen, zeigt die Ausgabe:
POST /get-online-features -> 200
body: {'user_id': '8a2f', 'features': {'user_segment': 'power_user', 'watch_count_30d': 47, 'last_genre': 'documentary'}}
Prompt, den das LLM erhalten würde:
System: Sie empfehlen Shows für einen Streaming-Dienst.
Benutzerkontext: segment=power_user, gesehen 47 Titel in den letzten 30 Tagen, letztes Genre: documentary.
Aufgabe: schlagen Sie 3 Titel in einer freundlichen, kurzen Nachricht vor.
Der Feature Store ist das Element, das „Benutzer 8a2f“ in einen strukturierten Kontext umwandelt, den das LLM nutzen kann.
Wo der Feature Store endet und die Vektordatenbank beginnt
Eine Vektordatenbank (Pinecone, Weaviate, pgvector) ist kein Feature Store, obwohl beide Systeme vor einem Modell bei der Inferenz stehen. Sie lösen unterschiedliche Abrufprobleme.
Ein echtes LLM-Stack verwendet beide. Die Vektordatenbank gibt die drei ähnlichsten vergangenen Betrachtungssitzungen zurück. Der Feature Store liefert das Segment des Benutzers und die aktuellen Zählungen. Der Prompt kombiniert beide.
Häufige Fehlmuster
Einige Muster, die häufig scheitern:
- Berechnung von Merkmalen innerhalb des Modellservices. Dieselbe Logik findet sich sowohl im Trainingsnotizbuch als auch in der API, was zu Abweichungen führt.
- Behandlung des Online-Speichers als Quelle der Wahrheit. Redis verliert Daten bei einem schlechten Neustart. Der Offline-Speicher ist kanonisch; der Online-Speicher ist ein Cache.
- Überspringen des Registers. Drei Teams definieren unabhängig active_user, und die Dashboards stimmen nicht mehr mit dem Modell überein.
- Bezeichnung einer Vektordatenbank als Feature Store. Sie kann keine entitätsbezogenen strukturierten Abfragen durchführen, und ein Prompt, der beides benötigt, wird ohnehin an zwei Systeme angeschlossen.
- Backfilling ohne zeitpunktbezogene Joins. Das Trainingsset sieht gut aus, das Produktionsmodell sieht kaputt aus, und die Lücke ist das Leck.
Vergleich mit Feast, Tecton und Databricks
Unsere ~200 Zeilen erfüllen denselben Zweck im Miniaturformat.
Feast ist der nächstgelegene Vergleich, wenn wir weiter im selben Muster arbeiten möchten, selbst gehostet. Tecton und Databricks sind die verwalteten Optionen und bieten explizite LLM-Funktionen (Tectons Feature Retrieval API für LLMs, Databricks Feature Serving für komplexe generative KI-Systeme). Die Wahl zwischen ihnen hängt hauptsächlich davon ab, wie viel wir selbst betreiben möchten und ob der Rest unseres Stacks bereits in Databricks lebt.
Fazit
Ein funktionierender Feature Store besteht aus fünf Komponenten: einem Register, einem Offline-Speicher, einem Online-Speicher, einem Materialisierungsschritt und einer Abruf-API. Der einmalige Aufbau zeigt, warum Produktionssysteme so aussehen, wie sie es tun. Er verdeutlicht auch, wo sich das Design für KI ändert: Der Online-Abrufpfad ist die Oberfläche, die das LLM nutzt, zeitpunktbezogene Joins sind wichtig, wenn wir trainieren oder evaluieren, und die Vektordatenbank steht neben dem Feature Store, nicht innerhalb davon.
Sobald wir diese Komponenten haben, besteht der Austausch unserer minimalen Version gegen Effektive Merkmalsauswahl: Fünf nützliche Python-Skripte für Ihre Projekte, Tecton oder Databricks hauptsächlich aus der Migration des Registers. Die Struktur des Systems bleibt gleich.
Nate Rosidi ist Datenwissenschaftler und in der Produktstrategie tätig. Er ist auch Dozent für Analytik und Gründer von StrataScratch, einer Plattform, die Datenwissenschaftlern hilft, sich auf Vorstellungsgespräche mit realen Fragen von Top-Unternehmen vorzubereiten. Nate schreibt über die neuesten Trends auf dem Arbeitsmarkt, gibt Interviewtipps, teilt Projekte zur Datenwissenschaft und behandelt alles rund um SQL.
„`
Quellen: kdnuggets
Bildquelle: KI generiert
🚀