diff --git a/README.md b/README.md new file mode 100644 index 0000000..63a6a2b --- /dev/null +++ b/README.md @@ -0,0 +1,165 @@ +# 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` с миграцией и простоем. + +## Быстрый старт + +```bash +./gradlew bootRun +``` + +Приложение поднимется на `http://localhost:8080`, Flyway применит миграции +`V1__create_core_metadata.sql` … `V4__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](https://console.cloud.google.com): +создать проект → включить **Books API** → **Public data** → создать API key. + +```yaml +# application.yml +google: + books: + api-key: ВАШ_КЛЮЧ +``` + +## Публичный API книг + +Единый динамический эндпоинт — потребитель сам выбирает поля через `?fields=`, +вместо получения "простыни" всех данных сразу. + +```bash +# Список книг с выбором конкретных полей +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-конструктора): + +```bash +curl "http://localhost:8080/api/metadata/objects/Book/fields?orgId=1" +``` + +## Обогащение метаданных + +Цепочка источников, каждый бесплатный и не требующий партнёрской регистрации +(кроме Google Books, которому нужен свободный API-ключ): + +| Порядок | Источник | Ключ нужен | Покрытие | +|---|---|---|---| +| 1 | Open Library | Нет | Международное, слабое для чисто русских изданий | +| 2 | Google Books | Да (бесплатный) | Хорошее, включая книги через ЛитРес | +| 3 | FantLab | Нет | Отличное для фантастики/фэнтези на русском | +| 4 | LibGen | Нет | Широкое, но ненадёжное; **выключено по умолчанию** | + +```bash +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`), включается +осознанно: + +```yaml +libgen: + enabled: true +``` + +Также может быть недоступен без VPN в зависимости от региона. + +### Почему не Litres + +Изначально рассматривался как основной источник (`catalitv2` API), но: +1. Требует партнёрской регистрации (`app_id`/`secret_key`) — не мгновенно. +2. Протокол сложный: обязательный SID даже для анонимного доступа, строгий + формат времени без дробных секунд, `sha`-подпись обязательна всегда. +3. **Официальная документация прямо запрещает** использование этого API + в партнёрских сервисах, перепродающих/переотдающих каталог третьим лицам — + именно то, чем является этот сервис. + +Решено не тратить силы на партнёрскую регистрацию, пока хватает бесплатных +источников. Код может быть переиспользован при пересмотре решения. + +## Provenance и история изменений + +Каждое значение в `mt_data` сопровождается записью в `sources_meta` (JSONB): + +```json +{"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. Требует + усиления перед реальным внешним использованием.