Files
food-ai/backend
dbastrikin 78f1c8bf76 feat: food search sheet with FTS+trgm, dish/recent endpoints, multilingual aliases
Backend:
- GET /dishes/search — hybrid FTS (english + simple) + trgm + ILIKE search
- GET /diary/recent — recently used dishes and products for the current user
- product search upgraded: FTS on canonical_name and product_aliases, ranked by GREATEST(ts_rank, similarity)
- importoff: collect product_name_ru/de/fr/... as product_aliases for multilingual search (e.g. "сникерс" → "Snickers")
- migrations: FTS + trgm indexes merged into 001_initial_schema.sql (002 removed)

Flutter:
- FoodSearchSheet: debounced search field, recently-used section, product/dish results, scan-photo and barcode chips
- DishPortionSheet: quick ½/1/1½/2 buttons + custom input
- + button in meal card now opens FoodSearchSheet instead of going directly to AI scan
- 7 new l10n keys across all 12 languages

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 15:28:29 +02:00
..

FoodAI Backend

Go REST API с авторизацией через Firebase и JWT, асинхронным распознаванием блюд через Gemini AI, и каталогом продуктов питания на базе OpenFoodFacts.

Стек

  • Go 1.25.5 — язык
  • chi v5 — HTTP-роутер
  • pgx v5 / pgxpool — драйвер PostgreSQL
  • goose v3 — миграции
  • golang-jwt/v5 — JWT
  • Firebase Admin SDK — верификация токенов
  • franz-go — Kafka-продюсер и консьюмер (pure Go)
  • watermill + watermill-kafka — маршрутизация событий (воркер)
  • envconfig — конфигурация через переменные окружения
  • testcontainers-go — интеграционные тесты с реальной БД
  • generative-ai-go / OpenAI API — распознавание блюд, генерация рецептов и меню
  • Pexels API — изображения для рецептов

Требования

  • Go 1.25+
  • Docker & Docker Compose
  • goose (go install github.com/pressly/goose/v3/cmd/goose@latest)
  • Файл firebase-credentials.json (сервисный аккаунт Firebase)

Быстрый старт

1. Переменные окружения

cp .env.example .env

Отредактируйте .env:

Переменная Описание По умолчанию
DATABASE_URL DSN подключения к PostgreSQL postgres://food_ai:food_ai_local@localhost:5433/food_ai?sslmode=disable
FIREBASE_CREDENTIALS_FILE Путь к JSON-ключу сервисного аккаунта Firebase ./firebase-credentials.json
JWT_SECRET Секрет для подписи JWT
JWT_ACCESS_DURATION TTL access-токена 15m
JWT_REFRESH_DURATION TTL refresh-токена 720h
PORT Порт HTTP-сервера 8080
ALLOWED_ORIGINS CORS-разрешённые источники http://localhost:3000
KAFKA_BROKERS Адреса Kafka-брокеров localhost:9093 (dev), kafka:9092 (Docker)
OPENAI_API_KEY API-ключ Gemini/OpenAI (распознавание, генерация)
PEXELS_API_KEY API-ключ Pexels (изображения для рецептов)

2. Запуск через Docker Compose

Поднимает PostgreSQL, Kafka, сервер и оба воркера:

OPENAI_API_KEY=your-key make docker-up

3. Запуск локально (инфраструктура в Docker)

# Поднять только PostgreSQL + Kafka
make dev-infra-up

# Применить миграции
make migrate-up

# Запустить всё локально (сервер + оба воркера + инфраструктура)
make dev

Или по отдельности в разных терминалах:

make run               # HTTP-сервер (порт 8080)
make run-worker-paid   # Воркер paid-очереди
make run-worker-free   # Воркер free-очереди

4. Сервер и воркеры

Два типа процессов работают независимо:

  • Сервер (cmd/server) — обслуживает HTTP API и SSE-стримы; публикует задания распознавания в Kafka.
  • Воркер (cmd/worker) — потребляет задания из Kafka, вызывает AI, сохраняет результаты в БД и оповещает сервер через pg_notify.

Воркеры можно масштабировать горизонтально — они разделят нагрузку через consumer group dish-recognition-workers.

Переменные окружения воркера:

Переменная Описание
DATABASE_URL DSN подключения к PostgreSQL
OPENAI_API_KEY API-ключ Gemini/OpenAI
KAFKA_BROKERS Адреса Kafka-брокеров

Команды

Сервер и тесты

Команда Описание
make run Запустить HTTP-сервер (dev)
make run-worker-paid Запустить воркер paid-очереди
make run-worker-free Запустить воркер free-очереди
make dev Поднять инфраструктуру + сервер + оба воркера
make dev-infra-up Поднять только PostgreSQL + Kafka
make dev-infra-down Остановить инфраструктуру
make test Unit-тесты (с race detector)
make test-integration Интеграционные тесты (требует Docker)
make lint Проверка через golangci-lint
make docker-up Полный Docker Compose (app + workers + infra)
make docker-down Остановить все контейнеры
make docker-logs Логи app-контейнера
make docker-logs-worker Логи worker-контейнеров
make migrate-up Применить миграции
make migrate-down Откатить последнюю миграцию
make migrate-status Статус миграций
make migrate-create name=<name> Создать новую миграцию
make import-off file=<path> [limit=N] [batch=N] [min-scans=N] Импортировать каталог из OpenFoodFacts JSONL/GZ

Импорт каталога продуктов

Каталог продуктов заполняется из дампа OpenFoodFacts.

# Скачать дамп (~66 ГБ, ~4.4 млн записей)
# https://world.openfoodfacts.org/data/openfoodfacts-products.jsonl.gz

# Через make (DSN берётся из .env)
make import-off file=openfoodfacts-products.jsonl.gz
make import-off file=openfoodfacts-products.jsonl.gz limit=10000 min-scans=0

# Или напрямую
go run ./cmd/importoff \
  -file=openfoodfacts-products.jsonl.gz \
  -dsn="$DATABASE_URL" \
  -limit=10000 \
  -min-scans=1

Флаги:

Флаг Описание По умолчанию
-file Путь к JSONL или JSONL.GZ файлу обязательный
-dsn PostgreSQL DSN (или $DATABASE_URL) $DATABASE_URL
-limit Остановиться после N принятых записей (0 = без ограничения) 0
-batch Записей на один upsert-батч 500
-min-scans Минимальное число уникальных сканирований (0 = без фильтра) 1

Алгоритм:

  1. Создаёт временную staging-таблицу без уникальных ограничений.
  2. Читает файл построчно (gzip-прозрачно); пропускает записи без баркода, названия или калорий.
  3. Дедуплицирует canonical_name внутри батча.
  4. Загружает батч через COPY (без ошибок на дублях).
  5. Обнуляет barcodes, уже занятые другой записью в каталоге.
  6. INSERT … ON CONFLICT (canonical_name) DO UPDATE — безопасное слияние.

Команда идемпотентна — можно запускать повторно и прерывать.

API

Публичные эндпоинты

Метод Путь Описание
GET /health Статус сервера и БД
GET /languages Список поддерживаемых языков
GET /units Справочник единиц измерения
GET /cuisines Список кухонь
GET /tags Теги рецептов и блюд
POST /auth/login Вход через Firebase ID-токен → JWT
POST /auth/refresh Обновить JWT по refresh-токену
POST /auth/logout Выход (инвалидация refresh-токена)

Защищённые эндпоинты (Bearer JWT)

Метод Путь Описание
GET/PUT /profile Профиль пользователя
GET /products/search?q=&limit= Поиск в каталоге продуктов
GET /products/barcode/{barcode} Поиск по баркоду (OpenFoodFacts как fallback)
GET/POST/DELETE /user-products, /user-products/{id} Продукты пользователя (холодильник)
POST /user-products/batch Массовое добавление продуктов
GET /diary?date=YYYY-MM-DD Записи дневника питания за дату
GET /diary/recent?limit=<n> Последние записи дневника (блюда и продукты)
POST/DELETE /diary, /diary/{id} Добавить / удалить запись дневника
GET /home/summary Сводка для главного экрана
GET /recommendations AI-рекомендации блюд
GET/POST/DELETE /saved-recipes, /saved-recipes/{id} Сохранённые рецепты
GET/PUT/DELETE /menu/items/{id} Меню на неделю
GET/POST/PATCH /shopping-list, /shopping-list/{i}/check Список покупок
GET /dishes/search?q=<query>&limit=<n> Поиск блюд (FTS + trgm)
GET /dishes/{id} Блюдо по ID
GET /recipes/{id} Рецепт по ID
POST /ai/recognize-receipt Распознать чек (синхронно)
POST /ai/recognize-products Распознать продукты на фото (синхронно)
POST /ai/recognize-dish Распознать блюдо — 202 + job_id (асинхронно)
GET /ai/jobs Задания за сегодня
GET /ai/jobs/history Все задания пользователя
GET /ai/jobs/{id} Результат задания
GET /ai/jobs/{id}/stream SSE-стрим обновлений задания
POST /ai/generate-menu Сгенерировать меню через AI

Асинхронное распознавание блюд

POST /ai/recognize-dish ставит задание в очередь и сразу возвращает:

{"job_id": "...", "queue_position": 2, "estimated_seconds": 18}

Статус отслеживается через SSE: GET /ai/jobs/{id}/stream — события queued, processing, done, failed.

При повторном открытии приложения результат можно получить через GET /ai/jobs/{id}.

Две очереди с разным приоритетом обслуживания:

  • ai.recognize.paid (3 партиции) — платные пользователи
  • ai.recognize.free (1 партиция) — бесплатные пользователи

Примеры запросов

Логин:

curl -X POST http://localhost:8080/auth/login \
  -H "Content-Type: application/json" \
  -d '{"firebase_token": "<FIREBASE_ID_TOKEN>"}'

Поиск продукта:

curl "http://localhost:8080/products/search?q=butter&limit=5" \
  -H "Authorization: Bearer <ACCESS_TOKEN>"

Поиск по баркоду:

curl http://localhost:8080/products/barcode/3017620422003 \
  -H "Authorization: Bearer <ACCESS_TOKEN>"

Добавить запись в дневник:

curl -X POST http://localhost:8080/diary \
  -H "Authorization: Bearer <ACCESS_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{"date": "2026-03-21", "meal_type": "lunch", "product_id": "...", "portion_g": 150}'

Структура проекта

backend/
├── cmd/
│   ├── server/          # HTTP-сервер (точка входа)
│   ├── worker/          # Async-воркер распознавания блюд (Kafka)
│   └── importoff/       # Импорт каталога продуктов из OpenFoodFacts JSONL
├── internal/
│   ├── domain/
│   │   ├── auth/        # Firebase-верификация, JWT, login/refresh/logout
│   │   ├── user/        # Профиль, калькулятор КБЖУ
│   │   ├── product/     # Каталог продуктов (поиск, баркод, OFF-адаптер)
│   │   ├── userproduct/ # Продукты пользователя (холодильник/список)
│   │   ├── recipe/      # Каталог рецептов с переводами
│   │   ├── savedrecipe/ # Сохранённые рецепты пользователя
│   │   ├── dish/        # Блюда, рецепты блюд, переводы
│   │   ├── menu/        # Меню на неделю, список покупок
│   │   ├── diary/       # Дневник питания
│   │   ├── recognition/ # Async распознавание: jobs, SSE-broker, Kafka-воркер
│   │   ├── recommendation/ # AI-рекомендации
│   │   └── home/        # Сводка главного экрана
│   ├── adapters/
│   │   ├── ai/          # Общие типы AI-ответов
│   │   ├── openai/      # Gemini/OpenAI клиент (распознавание, генерация)
│   │   ├── firebase/    # Верификация Firebase-токенов
│   │   ├── kafka/       # Продюсер и консьюмер (franz-go)
│   │   └── pexels/      # Поиск изображений через Pexels API
│   └── infra/
│       ├── config/      # Конфигурация (envconfig)
│       ├── database/    # pgxpool-соединение
│       ├── locale/      # Определение языка из заголовка Accept-Language
│       ├── middleware/  # RequestID, Logging, Recovery, CORS, Auth, Language
│       └── server/      # Chi-роутер со всеми маршрутами
├── migrations/
│   └── 001_initial_schema.sql   # Полная схема БД
├── tests/               # Интеграционные и unit-тесты по доменам
├── .env.example
├── docker-compose.yml
├── Dockerfile
└── Makefile

Схема БД

Вся схема содержится в одном файле migrations/001_initial_schema.sql. Основные таблицы:

  • users, refresh_tokens — пользователи и токены
  • products, product_categories, product_category_translations — каталог продуктов
  • user_products — продукты пользователя
  • ingredients, ingredient_translations, ingredient_aliases — справочник ингредиентов
  • dishes, dish_translations, recipes, recipe_steps, recipe_ingredients — блюда и рецепты
  • saved_recipes, saved_recipe_translations — сохранённые рецепты
  • meal_diary — дневник питания
  • recognition_jobs — задания AI-распознавания
  • cuisines, cuisine_translations, tags, tag_translations, units, unit_translations — справочники

Для каждой таблицы с пользовательским текстом используется паттерн {table}_translations (companion table). Базовые таблицы хранят английский canonical text, переводы — только в _translations таблицах.

Тесты

# Unit-тесты (с race detector)
make test

# Интеграционные тесты (PostgreSQL поднимается через testcontainers)
make test-integration

Покрытые пакеты: auth, diary, middleware, recognition, recipe, savedrecipe, user, userproduct, locale, menu.