Создание хранилищ признаков с нуля: минимальная рабочая реализация
Создайте пять компонентов, необходимых для любого хранилища характеристик, а затем посмотрите, как искусственный интеллект изменит дизайн.

# Введение
Большинство команд на собственном горьком опыте убеждаются в необходимости хранилища функций . Модель обнаружения мошенничества работает в ноутбуке, но незаметно дает сбой в рабочей среде. Агент службы поддержки дает общий ответ, потому что не знает, кто пользователь. Конвейер рекомендаций дублирует один и тот же расчет «затрат за 30 дней» в трех заданиях, и два из них не совпадают.
Хранилище признаков — это часть инфраструктуры , которая решает эти проблемы. Оно определяет признаки один раз, хранит их в двух форматах (один для обучения, другой для развертывания) и поддерживает их синхронизацию. Мы собираемся создать минимальное хранилище с нуля на Python , используя DuckDB , Parquet , Redis и FastAPI . Затем мы рассмотрим, как приложения ИИ меняют то, для чего мы его на самом деле используем.
Полный код достаточно короткий, чтобы мы могли рассмотреть каждый компонент.
# Что на самом деле решает магазин функций
Классический аргумент заключается в искажении результатов при подаче данных на обучающую выборку: SQL-запросы, использованные для создания обучающего набора данных, не совпадают с кодом, выполняемым при выводе результатов, поэтому значения расходятся. Эта проблема реальна, и стандартным решением является разделение данных на офлайн и онлайн этапы.
Современный подход шире. Агенты на основе больших языковых моделей (LLM) и конвейеры генерации с расширенным поиском (RAG) нуждаются в структурированном контексте пользователя во время вывода, при каждом запросе, менее чем за 10 мс. LLM не помнит, кто пользователь. Если нам нужен персонализированный результат, мы должны ввести в запрос уровень тарифного плана пользователя, недавнюю активность и состояние учетной записи, и нам нужна система, которая может быстро и стабильно возвращать эти значения. Именно это нам и предоставляет онлайн-хранилище и API поиска данных в хранилище признаков.
Поэтому мы разрабатываем решения для обоих случаев. Одни и те же пять компонентов обрабатывают как сценарий использования прогнозного машинного обучения, так и сценарий использования контекста LLM.
# Пять компонентов
- Реестр функций, определяющий функции в виде кода.
- Офлайн-магазин на Parquet, данные о котором запрашиваются из DuckDB, предназначен для обучения и замещения вакансий.
- Интернет-магазин на Redis для поиска информации с низкой задержкой при выполнении инференции.
- Конвейер материализации, который передает последние значения из офлайн-режима в онлайн.
- Сервис FastAPI, предоставляющий типизированный API для получения данных.
# Пример работы: Персонализированная система рекомендаций для получения степени магистра права
Мы управляем стриминговым сервисом. Когда пользователь открывает приложение, LLM генерирует короткое персонализированное сообщение «что посмотреть дальше». Для этого LLM необходимы три вещи о пользователе:
| Особенность | Тип | Свежесть |
|---|---|---|
| пользовательский_сегмент | нить | ежедневно |
| watch_count_30d | инт | почасово |
| последний_жанр | нить | за событие |
Сущность называется user_id. Мы зарегистрируем эти три функции, материализуем их и предоставим LLM по запросу.
// 1. Определение реестра функций
Реестр — это просто место, где функции объявляются один раз, с указанием их сущности, типа данных и источника. Мы используем класс данных.
from dataclasses import dataclass from typing import Literal @dataclass(frozen=True) class Feature: name: str entity: str dtype: Literal[«int», «float», «str»] source: str # путь к файлу Parquet или представлению SQL 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»), }
Полный код можно найти здесь.
При запуске программы отображается следующий вывод:
Зарегистрированные признаки: 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
Это контракт. Все остальные компоненты считывают данные из REGISTRY, поэтому переименование функции, изменение её типа данных или указание на новый источник происходят в одном месте. В производственных системах это может быть YAML-файл или модуль Python, добавленный в репозиторий Git, с проверкой кода при каждом изменении.
// 2. Создание офлайн-хранилища с использованием DuckDB и Parquet
В офлайн-хранилище хранится полная история значений каждого параметра. В качестве уровня хранения мы используем файлы Parquet, а в качестве механизма запросов — DuckDB. DuckDB считывает файлы Parquet напрямую, что означает отсутствие необходимости в отдельной базе данных.
Вот пример кода:
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()
Полный код можно найти здесь.
| ID пользователя | event_timestamp | пользовательский_сегмент | watch_count_30d | последний_жанр |
|---|---|---|---|---|
| 8a2f | 2026-05-05 12:00:00 | повседневный | 22 | NaN |
| б13с | 2026-05-07 20:00:00 | повседневный | 5 | триллер |
| 8a2f | 2026-05-07 22:00:00 | power_user | 47 | документальный |
Соединение AsOf — это соединение по моменту времени. Для каждой строки сущности оно выбирает самое последнее значение признака, где временная метка признака совпадает или предшествует временной метке события. Именно это предотвращает утечку — ситуацию, когда обучающая строка создается со значением признака, которого еще не существовало в момент прогнозирования.
Объединение данных в определенный момент времени по-прежнему является правильным решением для любой модели, которую мы планируем обучать или дорабатывать. В случае использования LLM исключительно на этапе вывода результатов, мы можем никогда не вызывать эту функцию. Нам по-прежнему необходимо офлайн-хранилище, поскольку именно оттуда поступают данные для заполнения пропусков, оценочные наборы данных и результаты аудита.
// 3. Настройка интернет-магазина на Redis
Интернет-магазин хранит только самое актуальное значение для каждой сущности. Redis является стандартным выбором, поскольку поиск хеша занимает менее миллисекунды.
import json import fakeredis # использовать redis.Redis() для проверки реального сервера в продакшене 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)}
Полный код можно найти здесь.
read_online -> {'user_segment': 'power_user', 'watch_count_30d': 47, 'last_genre': 'documentary'} missing key -> {'user_segment': None}
Ключевая структура — entity:entity_id. Значение — хеш, по одному полю на каждый объект. Один запрос HMGET возвращает все запрошенные объекты за один цикл. На локальном экземпляре Redis с тремя объектами это занимает менее 1 мс.
// 4. Запуск конвейера материализации
Материализация перемещает значения из офлайн-режима в онлайн. В реальной системе это происходит по расписанию (Airflow, cron, потоковая обработка данных). Здесь же это функция.
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)
Полный код можно найти здесь.
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'}
В предложении QUALIFY хранится последняя строка для каждой сущности. Мы объединяем все характеристики одного пользователя в одну запись в Redis, чтобы сократить количество обращений к серверу. Запускайте это с той периодичностью, которая необходима для каждой характеристики: ежечасно для watch_count_30d, почти в реальном времени для last_genre, ежедневно для user_segment. В реальной реализации реестр — подходящее место для кодирования этой периодичности.
// 5. Предоставление доступа к службе получения данных FastAPI
Сервис извлечения данных представляет собой производственную поверхность. Именно это используется в приложении LLM.
f = resp.json()[«features»] print(«nЗапрос, который получит LLM:») print( f» Система: Вы рекомендуете сериалы для потокового сервиса.n» f» Контекст пользователя: segment={f['user_segment']}, » f»просмотрел {f['watch_count_30d']} фильмов за последние 30 дней, » f»последний просмотренный жанр: {f['last_genre']}.n» f» Задача: предложить 3 фильма в дружелюбном, коротком сообщении.» )
Полный код можно найти здесь.
POST /get-online-features -> 200 body: {'user_id': '8a2f', 'features': {'user_segment': 'power_user', 'watch_count_30d': 47, 'last_genre': 'documentary'}} Запрос, который получит LLM: Система: Вы рекомендуете сериалы для стримингового сервиса. Контекст пользователя: сегмент=power_user, посмотрел 47 фильмов за последние 30 дней, последний просмотренный жанр: документальный. Задача: предложить 3 фильма в дружелюбном, коротком сообщении.
Хранилище функций — это тот элемент, который превращает «пользователя 8a2f» в структурированный контекст, который может использовать LLM.
# Где заканчивается хранилище функций и начинается векторная база данных
Векторная база данных ( Pinecone , Weaviate , pgvector ) не является хранилищем признаков, хотя обе они используются в качестве основы для вывода модели. Они решают разные задачи поиска.
В реальной системе LLM используются оба подхода. Векторная база данных возвращает три наиболее похожих предыдущих сеанса просмотра. Хранилище признаков возвращает сегмент пользователя и количество недавних просмотров. Запрос объединяет их.
# Распространенные антипаттерны
Вот несколько шаблонов, которые, как мы постоянно видим, не работают:
- Вычисление характеристик внутри сервиса модели. Та же логика в итоге оказывается в обучающем блокноте и API, и эти два определения расходятся в течение квартала.
- Рассматривайте интернет-магазин как источник достоверной информации. Redis теряет данные при некорректном перезапуске. Офлайн-магазин является каноническим; интернет-магазин — это кэш.
- Пропуск реестра. Три команды независимо друг от друга определяют active_user, и панели мониторинга перестают соответствовать модели.
- Называть векторную базу данных хранилищем признаков — это неправильно. Она не может выполнять структурированный поиск по ключу сущности, а запрос, требующий и того, и другого, в любом случае будет подключен к двум системам.
- Заполнение пробелов без соединения данных в определенный момент времени. Тренировочный набор данных выглядит отлично, производственная модель выглядит некорректно, а пробел — это утечка.
# Сравнение с Feast, Tecton и Databricks
Наши примерно 200 линий выполняют ту же работу в миниатюре.
Если мы хотим продолжить работу по той же схеме — с самостоятельным размещением — то Feast является наиболее близким аналогом. Tecton и Databricks — это управляемые решения с явными функциями LLM (API Tecton для получения признаков для LLM, Feature Serving от Databricks для сложных генеративных систем ИИ). Выбор между ними в основном сводится к вопросу о том, насколько мы хотим управлять ими самостоятельно и размещена ли остальная часть нашего стека уже в Databricks.
# Заключение
Рабочее хранилище признаков состоит из пяти компонентов: реестра, офлайн-хранилища, онлайн-хранилища, этапа материализации и API для извлечения данных. Создание такого хранилища один раз позволяет понять, почему производственные системы выглядят именно так. Это также показывает, где меняется дизайн для ИИ: онлайн-путь извлечения данных — это поверхность, с которой взаимодействует LLM, объединения данных в определенный момент времени имеют значение при обучении или оценке, а векторная база данных располагается рядом с хранилищем признаков, а не внутри него.
Как только у нас появятся эти компоненты, замена нашей минимальной версии на Feast, Tecton или Databricks будет в основном заключаться в миграции реестра. Структура системы останется прежней.
Нейт Розиди — специалист по анализу данных и продуктовой стратегии. Он также является адъюнкт-профессором, преподающим аналитику, и основателем StrataScratch, платформы, помогающей специалистам по анализу данных готовиться к собеседованиям с помощью реальных вопросов от ведущих компаний. Нейт пишет о последних тенденциях на рынке труда, дает советы по прохождению собеседований, делится проектами по анализу данных и освещает все аспекты SQL.
Источник: www.kdnuggets.com
Похожие записи
Оцените материал:
Похожие записи
Компания Trovo Health провела ребрендинг и теперь называется Thesis Care, привлекла 45 миллионов долларов инвестиций и получила другие новости о финансировании | MobiHealthNews
31.03.2026
Как экологически чистый водород может обеспечить энергией различные отрасли промышленности, от сталелитейного производства до сельского хозяйства.
15.12.2025
Компания SpaceX официально установила цену акций на уровне 135 долларов, проведя крупнейшее IPO в истории.
12.06.2026Присоединяйтесь и подпишитесь на рассылку самых свежих новостей по Email
Получайте свежие новости и идеи на почту. Без спама — только самое интересное.
Нажимая «Подписаться», вы соглашаетесь с политикой конфиденциальности.
