@malexple malexple authored 15 hours ago
gradle/ wrapper init app 17 hours ago
src/ main delete litres 15 hours ago
.gitignore init app 17 hours ago
README.md add README.md 15 hours ago
build.gradle init app 17 hours ago
gradle.properties init app 17 hours ago
gradlew init app 17 hours ago
gradlew.bat init app 17 hours ago
settings.gradle init app 17 hours ago
README.md

Book Metadata Service

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

Идея: в русскоязычном сегменте нет удобного открытого аналога Google Books API — этот сервис закрывает эту нишу, агрегируя данные из нескольких источников и отдавая их через единый динамический эндпоинт, где потребитель сам выбирает нужные поля.

Стек

  • Java 21, Spring Boot 3.4, Gradle (Groovy DSL)
  • PostgreSQL + Flyway для миграций
  • Hibernate 6 с JSONB-маппингом через hypersistence-utils
  • Модель данных — pivot/EAV-архитектура (вдохновлена whitepaper Force.com Multitenancy), адаптированная под книжный каталог

Архитектура данных

Вместо жёсткой реляционной схемы под каждый тип контента используется гибкая метамодель:

  • mt_objects — типы контента (Book, Author, Magazine)
  • mt_fields — динамический справочник полей для каждого типа, с привязкой к конкретному flex-слоту (field_num)
  • mt_data — хранилище записей с 50 универсальными колонками value0..value49; какое поле лежит в каком слоте — определяется через mt_fields
  • mt_indexes — типизированный индекс (string/num/date) для полей с is_indexed=true, чтобы поиск не сканировал текстовые слоты
  • mt_field_history — история изменений значений при повторном обогащении
  • sources_meta (JSONB-колонка в mt_data) — provenance: из какого источника взято каждое значение, когда и с какой уверенностью (confidence)
  • unresolved_lookup — очередь ISBN/названий, не найденных ни в одном источнике
  • mt_attachment — задел под будущую загрузку PDF/DjVu для OCR-конвейера
  • api_keys — минимальная таблица API-ключей для доступа к публичному API

Преимущество такой модели: добавить новое поле для типа контента — это INSERT в mt_fields, а не ALTER TABLE с миграцией и простоем.

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

./gradlew bootRun

Приложение поднимется на http://localhost:8080, Flyway применит миграции V1__create_core_metadata.sqlV4__support_tables.sql автоматически.

  • Swagger UI: http://localhost:8080/swagger-ui.html
  • OpenAPI JSON: http://localhost:8080/api-docs

Тестовый API-ключ уже засеян миграцией: dev-local-key-change-me (см. таблицу api_keys).

Обязательная настройка перед использованием обогащения

Google Books API требует собственный ключ (анонимная квота исчерпана Google глобально) — получить бесплатно на console.cloud.google.com: создать проект → включить Books APIPublic data → создать API key.

# application.yml
google:
  books:
    api-key: ВАШ_КЛЮЧ

Публичный API книг

Единый динамический эндпоинт — потребитель сам выбирает поля через ?fields=, вместо получения "простыни" всех данных сразу.

# Список книг с выбором конкретных полей
curl "http://localhost:8080/api/v1/books?apiKey=dev-local-key-change-me&fields=title,isbn,author_name"

# Поиск по индексированному полю
curl "http://localhost:8080/api/v1/books/search?apiKey=dev-local-key-change-me&field=ISBN&value=9785041558389"

# Получить книгу по GUID
curl "http://localhost:8080/api/v1/books/{guid}?apiKey=dev-local-key-change-me&fields=title,description"

# Создать книгу вручную
curl -X POST "http://localhost:8080/api/v1/books?apiKey=dev-local-key-change-me" \
  -H "Content-Type: application/json" \
  -d '{"values": {"Title": "Мастер и Маргарита", "ISBN": "9785041558389"}, "source": "manual"}'

Список доступных полей объекта — через Metadata API (источник для будущего UI-конструктора):

curl "http://localhost:8080/api/metadata/objects/Book/fields?orgId=1"

Обогащение метаданных

Цепочка источников, каждый бесплатный и не требующий партнёрской регистрации (кроме Google Books, которому нужен свободный API-ключ):

Порядок Источник Ключ нужен Покрытие
1 Open Library Нет Международное, слабое для чисто русских изданий
2 Google Books Да (бесплатный) Хорошее, включая книги через ЛитРес
3 FantLab Нет Отличное для фантастики/фэнтези на русском
4 LibGen Нет Широкое, но ненадёжное; выключено по умолчанию
curl -X POST "http://localhost:8080/api/v1/enrichment/public/by-isbn?apiKey=dev-local-key-change-me&isbn=9785041558389"

Логика: идём по источникам по порядку, останавливаемся на первом найденном. Если ничего не найдено — ISBN сохраняется в unresolved_lookup со списком attempted_sources, чтобы можно было разобрать вручную позже.

Про LibGen

Технически открытый и мощный источник (структурированный поиск по ISBN), но представляет собой теневую библиотеку — юридически серая зона даже для одних метаданных. Выключен по умолчанию (libgen.enabled: false), включается осознанно:

libgen:
  enabled: true

Также может быть недоступен без VPN в зависимости от региона.

Почему не Litres

Изначально рассматривался как основной источник (catalitv2 API), но:

  1. Требует партнёрской регистрации (app_id/secret_key) — не мгновенно.
  2. Протокол сложный: обязательный SID даже для анонимного доступа, строгий формат времени без дробных секунд, sha-подпись обязательна всегда.
  3. Официальная документация прямо запрещает использование этого API в партнёрских сервисах, перепродающих/переотдающих каталог третьим лицам — именно то, чем является этот сервис.

Решено не тратить силы на партнёрскую регистрацию, пока хватает бесплатных источников. Код может быть переиспользован при пересмотре решения.

Provenance и история изменений

Каждое значение в mt_data сопровождается записью в sources_meta (JSONB):

{"2": {"source": "googlebooks", "updated_at": "2026-08-18T00:32:00Z", "confidence": 0.7}}

Ключ — номер flex-слота (field_num). При повторном обогащении старое значение поля сохраняется в mt_field_history перед перезаписью.

Известные ограничения / что дальше

  • OCR-конвейер (Tesseract) для PDF/DjVu — в разработке. Таблица mt_attachment уже заложена под эту задачу.
  • UI-конструктор (Thymeleaf) для визуального выбора полей — не реализован, Metadata API уже готов как источник данных для него.
  • Резолюция авторов — сейчас AuthorName пишется как текст, AuthorId (lookup на объект Author) не заполняется автоматически.
  • Планировщик для периодического обхода unresolved_lookup и повторных попыток обогащения — не реализован.
  • API-ключи — хранятся в открытом виде в БД, без rate-limiting. Требует усиления перед реальным внешним использованием.