mirror of
https://github.com/trustgraph-ai/trustgraph.git
synced 2026-04-25 08:26:21 +02:00
993 lines
49 KiB
Markdown
993 lines
49 KiB
Markdown
|
|
---
|
|||
|
|
layout: default
|
|||
|
|
title: "Техническая спецификация по загрузке больших документов"
|
|||
|
|
parent: "Russian (Beta)"
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# Техническая спецификация по загрузке больших документов
|
|||
|
|
|
|||
|
|
> **Beta Translation:** This document was translated via Machine Learning and as such may not be 100% accurate. All non-English languages are currently classified as Beta.
|
|||
|
|
|
|||
|
|
## Обзор
|
|||
|
|
|
|||
|
|
Эта спецификация рассматривает проблемы масштабируемости и удобства использования при загрузке
|
|||
|
|
больших документов в TrustGraph. Текущая архитектура рассматривает загрузку документов
|
|||
|
|
как единую атомарную операцию, что приводит к перегрузке памяти на нескольких этапах
|
|||
|
|
процесса и не предоставляет пользователям обратной связи или возможностей восстановления.
|
|||
|
|
|
|||
|
|
Эта реализация нацелена на следующие сценарии использования:
|
|||
|
|
|
|||
|
|
1. **Обработка больших PDF-файлов**: Загрузка и обработка PDF-файлов объемом в сотни мегабайт
|
|||
|
|
без переполнения памяти
|
|||
|
|
2. **Возобновляемая загрузка**: Возможность продолжить прерванную загрузку с того места,
|
|||
|
|
где она была остановлена, а не перезапускать ее.
|
|||
|
|
3. **Информативность о ходе выполнения**: Предоставление пользователям информации в режиме реального времени
|
|||
|
|
о ходе загрузки и обработки.
|
|||
|
|
4. **Эффективная обработка памяти**: Обработка документов потоковым способом
|
|||
|
|
без хранения целых файлов в памяти.
|
|||
|
|
|
|||
|
|
## Цели
|
|||
|
|
|
|||
|
|
**Инкрементная загрузка**: Поддержка загрузки документов по частям через REST и WebSocket
|
|||
|
|
**Возобновляемые передачи**: Возможность восстановления после прерванных загрузок
|
|||
|
|
**Информативность о ходе выполнения**: Предоставление клиентам информации о ходе загрузки/обработки
|
|||
|
|
**Эффективность использования памяти**: Исключение полной буферизации документов на протяжении всего процесса
|
|||
|
|
**Обратная совместимость**: Существующие процессы загрузки небольших документов продолжают работать без изменений
|
|||
|
|
**Потоковая обработка**: Декодирование PDF и разбиение текста на фрагменты выполняются в потоковом режиме
|
|||
|
|
|
|||
|
|
## Описание
|
|||
|
|
|
|||
|
|
### Текущая архитектура
|
|||
|
|
|
|||
|
|
Процесс отправки документов проходит по следующему пути:
|
|||
|
|
|
|||
|
|
1. **Клиент** отправляет документ через REST (`POST /api/v1/librarian`) или WebSocket
|
|||
|
|
2. **API Gateway** получает полный запрос с содержимым документа в кодировке base64
|
|||
|
|
3. **LibrarianRequestor** преобразует запрос в сообщение Pulsar
|
|||
|
|
4. **Librarian Service** получает сообщение, декодирует документ в память
|
|||
|
|
5. **BlobStore** загружает документ в Garage/S3
|
|||
|
|
6. **Cassandra** хранит метаданные со ссылкой на объект
|
|||
|
|
7. Для обработки: документ извлекается из S3, декодируется, разбивается на фрагменты — все в памяти
|
|||
|
|
|
|||
|
|
Основные файлы:
|
|||
|
|
Точка входа REST/WebSocket: `trustgraph-flow/trustgraph/gateway/service.py`
|
|||
|
|
Основной модуль Librarian: `trustgraph-flow/trustgraph/librarian/librarian.py`
|
|||
|
|
Хранилище объектов: `trustgraph-flow/trustgraph/librarian/blob_store.py`
|
|||
|
|
Таблицы Cassandra: `trustgraph-flow/trustgraph/tables/library.py`
|
|||
|
|
Схема API: `trustgraph-base/trustgraph/schema/services/library.py`
|
|||
|
|
|
|||
|
|
### Текущие ограничения
|
|||
|
|
|
|||
|
|
Текущая конструкция имеет несколько взаимосвязанных проблем, связанных с памятью и удобством использования:
|
|||
|
|
|
|||
|
|
1. **Атомарная операция загрузки**: Весь документ должен быть передан в одном запросе.
|
|||
|
|
Загрузка больших документов требует длительных запросов без индикации хода выполнения
|
|||
|
|
и без механизма повторной отправки в случае сбоя соединения.
|
|||
|
|
|
|||
|
|
2. **Дизайн API**: И REST, и WebSocket API ожидают получения всего документа
|
|||
|
|
в одном сообщении. Схема (`LibrarianRequest`) имеет одно поле `content`
|
|||
|
|
для хранения всего документа в кодировке base64.
|
|||
|
|
|
|||
|
|
3. **Память Librarian**: Сервис Librarian декодирует весь документ
|
|||
|
|
в память перед загрузкой в S3. Для PDF-файла объемом 500 МБ это означает
|
|||
|
|
выделение 500 МБ + в оперативной памяти.
|
|||
|
|
|
|||
|
|
4. **Память декодера PDF**: При начале обработки декодер PDF-файла загружает
|
|||
|
|
весь PDF-файл в память для извлечения текста. Такие библиотеки, как PyPDF,
|
|||
|
|
обычно требуют полного доступа к документу.
|
|||
|
|
|
|||
|
|
5. **Память разбивающего на фрагменты**: Разбивающий на фрагменты получает весь извлеченный текст
|
|||
|
|
и хранит его в памяти при создании фрагментов.
|
|||
|
|
|
|||
|
|
**Пример влияния на память** (PDF-файл объемом 500 МБ):
|
|||
|
|
Gateway: ~700 МБ (дополнительный объем для кодирования base64)
|
|||
|
|
Librarian: ~500 МБ (декодированные байты)
|
|||
|
|
Декодер PDF: ~500 МБ + буферы извлечения
|
|||
|
|
Разбивающий на фрагменты: извлеченный текст (переменная, потенциально 100 МБ+)
|
|||
|
|
|
|||
|
|
Общий пиковый объем используемой памяти может превысить 2 ГБ для одного большого документа.
|
|||
|
|
|
|||
|
|
## Технический дизайн
|
|||
|
|
|
|||
|
|
### Принципы проектирования
|
|||
|
|
|
|||
|
|
1. **API Facade**: Все взаимодействия с клиентом осуществляются через API Librarian. Клиенты
|
|||
|
|
не имеют прямого доступа к хранилищу S3/Garage и не знают о его существовании.
|
|||
|
|
|
|||
|
|
2. **Многокомпонентная загрузка в S3**: Используется стандартная многокомпонентная загрузка в S3.
|
|||
|
|
Это широко поддерживается в системах, совместимых с S3 (AWS S3, MinIO, Garage,
|
|||
|
|
Ceph, DigitalOcean Spaces, Backblaze B2 и т. д.), что обеспечивает переносимость.
|
|||
|
|
|
|||
|
|
3. **Атомарное завершение**: Многокомпонентные загрузки в S3 по своей природе являются атомарными — загруженные
|
|||
|
|
фрагменты невидимы до вызова `CompleteMultipartUpload`. Не требуются временные
|
|||
|
|
файлы или операции переименования.
|
|||
|
|
|
|||
|
|
4. **Отслеживаемое состояние**: Сессии загрузки отслеживаются в Cassandra, что обеспечивает
|
|||
|
|
информацию о незавершенных загрузках и позволяет восстановить загрузку.
|
|||
|
|
|
|||
|
|
### Поток загрузки по частям
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
Client Librarian API S3/Garage
|
|||
|
|
│ │ │
|
|||
|
|
│── begin-upload ───────────►│ │
|
|||
|
|
│ (metadata, size) │── CreateMultipartUpload ────►│
|
|||
|
|
│ │◄── s3_upload_id ─────────────│
|
|||
|
|
│◄── upload_id ──────────────│ (store session in │
|
|||
|
|
│ │ Cassandra) │
|
|||
|
|
│ │ │
|
|||
|
|
│── upload-chunk ───────────►│ │
|
|||
|
|
│ (upload_id, index, data) │── UploadPart ───────────────►│
|
|||
|
|
│ │◄── etag ─────────────────────│
|
|||
|
|
│◄── ack + progress ─────────│ (store etag in session) │
|
|||
|
|
│ ⋮ │ ⋮ │
|
|||
|
|
│ (repeat for all chunks) │ │
|
|||
|
|
│ │ │
|
|||
|
|
│── complete-upload ────────►│ │
|
|||
|
|
│ (upload_id) │── CompleteMultipartUpload ──►│
|
|||
|
|
│ │ (parts coalesced by S3) │
|
|||
|
|
│ │── store doc metadata ───────►│ Cassandra
|
|||
|
|
│◄── document_id ────────────│ (delete session) │
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Клиент никогда не взаимодействует с S3 напрямую. Библиотека преобразует
|
|||
|
|
наши API загрузки по частям в многокомпонентные операции S3 внутри себя.
|
|||
|
|
|
|||
|
|
### Операции API библиотеки
|
|||
|
|
|
|||
|
|
#### `begin-upload`
|
|||
|
|
|
|||
|
|
Инициализация сессии загрузки по частям.
|
|||
|
|
|
|||
|
|
Запрос:
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"operation": "begin-upload",
|
|||
|
|
"document-metadata": {
|
|||
|
|
"id": "doc-123",
|
|||
|
|
"kind": "application/pdf",
|
|||
|
|
"title": "Large Document",
|
|||
|
|
"user": "user-id",
|
|||
|
|
"tags": ["tag1", "tag2"]
|
|||
|
|
},
|
|||
|
|
"total-size": 524288000,
|
|||
|
|
"chunk-size": 5242880
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Ответ:
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"upload-id": "upload-abc-123",
|
|||
|
|
"chunk-size": 5242880,
|
|||
|
|
"total-chunks": 100
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Библиотекарь:
|
|||
|
|
1. Генерирует уникальный `upload_id` и `object_id` (UUID для хранения объектов)
|
|||
|
|
2. Вызывает `CreateMultipartUpload` S3, получает `s3_upload_id`
|
|||
|
|
3. Создает запись сессии в Cassandra
|
|||
|
|
4. Возвращает `upload_id` клиенту
|
|||
|
|
|
|||
|
|
#### `upload-chunk`
|
|||
|
|
|
|||
|
|
Загрузка одного фрагмента.
|
|||
|
|
|
|||
|
|
Запрос:
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"operation": "upload-chunk",
|
|||
|
|
"upload-id": "upload-abc-123",
|
|||
|
|
"chunk-index": 0,
|
|||
|
|
"content": "<base64-encoded-chunk>"
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Ответ:
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"upload-id": "upload-abc-123",
|
|||
|
|
"chunk-index": 0,
|
|||
|
|
"chunks-received": 1,
|
|||
|
|
"total-chunks": 100,
|
|||
|
|
"bytes-received": 5242880,
|
|||
|
|
"total-bytes": 524288000
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Библиотекарь:
|
|||
|
|
1. Ищет сессию по `upload_id`
|
|||
|
|
2. Проверяет право собственности (пользователь должен совпадать с создателем сессии)
|
|||
|
|
3. Вызывает S3 `UploadPart` с данными фрагмента, получает `etag`
|
|||
|
|
4. Обновляет запись сессии с индексом фрагмента и etag
|
|||
|
|
5. Возвращает прогресс клиенту
|
|||
|
|
|
|||
|
|
Неудачные фрагменты можно повторить - просто отправьте тот же `chunk-index` снова.
|
|||
|
|
|
|||
|
|
#### `complete-upload`
|
|||
|
|
|
|||
|
|
Завершите загрузку и создайте документ.
|
|||
|
|
|
|||
|
|
Запрос:
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"operation": "complete-upload",
|
|||
|
|
"upload-id": "upload-abc-123"
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Ответ:
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"document-id": "doc-123",
|
|||
|
|
"object-id": "550e8400-e29b-41d4-a716-446655440000"
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Библиотекарь:
|
|||
|
|
1. Ищет сессию, проверяет, все ли фрагменты получены.
|
|||
|
|
2. Вызывает S3 `CompleteMultipartUpload` с информацией о частях (S3 объединяет части
|
|||
|
|
внутри себя - нулевая стоимость по памяти для библиотекаря).
|
|||
|
|
3. Создает запись документа в Cassandra с метаданными и ссылкой на объект.
|
|||
|
|
4. Удаляет запись сессии загрузки.
|
|||
|
|
5. Возвращает идентификатор документа клиенту.
|
|||
|
|
|
|||
|
|
#### `abort-upload`
|
|||
|
|
|
|||
|
|
Отмена загрузки, находящейся в процессе.
|
|||
|
|
|
|||
|
|
Запрос:
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"operation": "abort-upload",
|
|||
|
|
"upload-id": "upload-abc-123"
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Библиотекарь:
|
|||
|
|
1. Вызывает S3 `AbortMultipartUpload` для очистки разделов.
|
|||
|
|
2. Удаляет запись сессии из Cassandra.
|
|||
|
|
|
|||
|
|
#### `get-upload-status`
|
|||
|
|
|
|||
|
|
Проверка статуса загрузки (для возможности возобновления).
|
|||
|
|
|
|||
|
|
Запрос:
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"operation": "get-upload-status",
|
|||
|
|
"upload-id": "upload-abc-123"
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Ответ:
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"upload-id": "upload-abc-123",
|
|||
|
|
"state": "in-progress",
|
|||
|
|
"chunks-received": [0, 1, 2, 5, 6],
|
|||
|
|
"missing-chunks": [3, 4, 7, 8],
|
|||
|
|
"total-chunks": 100,
|
|||
|
|
"bytes-received": 36700160,
|
|||
|
|
"total-bytes": 524288000
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### `list-uploads`
|
|||
|
|
|
|||
|
|
Список неполных загрузок для пользователя.
|
|||
|
|
|
|||
|
|
Запрос:
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"operation": "list-uploads"
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Ответ:
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"uploads": [
|
|||
|
|
{
|
|||
|
|
"upload-id": "upload-abc-123",
|
|||
|
|
"document-metadata": { "title": "Large Document", ... },
|
|||
|
|
"progress": { "chunks-received": 43, "total-chunks": 100 },
|
|||
|
|
"created-at": "2024-01-15T10:30:00Z"
|
|||
|
|
}
|
|||
|
|
]
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Загрузка данных сессии
|
|||
|
|
|
|||
|
|
Отслеживание текущих загрузок в Cassandra:
|
|||
|
|
|
|||
|
|
```sql
|
|||
|
|
CREATE TABLE upload_session (
|
|||
|
|
upload_id text PRIMARY KEY,
|
|||
|
|
user text,
|
|||
|
|
document_id text,
|
|||
|
|
document_metadata text, -- JSON: title, kind, tags, comments, etc.
|
|||
|
|
s3_upload_id text, -- internal, for S3 operations
|
|||
|
|
object_id uuid, -- target blob ID
|
|||
|
|
total_size bigint,
|
|||
|
|
chunk_size int,
|
|||
|
|
total_chunks int,
|
|||
|
|
chunks_received map<int, text>, -- chunk_index → etag
|
|||
|
|
created_at timestamp,
|
|||
|
|
updated_at timestamp
|
|||
|
|
) WITH default_time_to_live = 86400; -- 24 hour TTL
|
|||
|
|
|
|||
|
|
CREATE INDEX upload_session_user ON upload_session (user);
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Поведение TTL:**
|
|||
|
|
Сессии истекают через 24 часа, если не завершены.
|
|||
|
|
Когда TTL Cassandra истекает, запись сессии удаляется.
|
|||
|
|
Оставшиеся фрагменты S3 очищаются политикой жизненного цикла S3 (настраивается на бакете).
|
|||
|
|
|
|||
|
|
### Обработка ошибок и атомарность
|
|||
|
|
|
|||
|
|
**Ошибка загрузки фрагмента:**
|
|||
|
|
Клиент повторяет загрузку неудачного фрагмента (с теми же `upload_id` и `chunk-index`).
|
|||
|
|
Операция `UploadPart` S3 является идемпотентной для одного и того же номера фрагмента.
|
|||
|
|
Сессия отслеживает, какие фрагменты были успешно загружены.
|
|||
|
|
|
|||
|
|
**Разрыв соединения клиента во время загрузки:**
|
|||
|
|
Сессия остается в Cassandra с записью о полученных фрагментах.
|
|||
|
|
Клиент может вызвать `get-upload-status`, чтобы узнать, что отсутствует.
|
|||
|
|
Возобновление путем загрузки только отсутствующих фрагментов, затем `complete-upload`.
|
|||
|
|
|
|||
|
|
**Ошибка полной загрузки:**
|
|||
|
|
Операция `CompleteMultipartUpload` S3 является атомарной - либо успешно завершается полностью, либо не удается.
|
|||
|
|
В случае сбоя фрагменты остаются, и клиент может повторить `complete-upload`.
|
|||
|
|
Ни один частичный документ никогда не отображается.
|
|||
|
|
|
|||
|
|
**Истечение срока действия сессии:**
|
|||
|
|
TTL Cassandra удаляет запись сессии через 24 часа.
|
|||
|
|
Политика жизненного цикла бакета S3 очищает неполные многокомпонентные загрузки.
|
|||
|
|
Не требуется ручная очистка.
|
|||
|
|
|
|||
|
|
### Атомарность многокомпонентной загрузки S3
|
|||
|
|
|
|||
|
|
Многокомпонентные загрузки S3 обеспечивают встроенную атомарность:
|
|||
|
|
|
|||
|
|
1. **Фрагменты невидимы**: Загруженные фрагменты не могут быть доступны в качестве объектов.
|
|||
|
|
Они существуют только как части незавершенной многокомпонентной загрузки.
|
|||
|
|
|
|||
|
|
2. **Атомарное завершение**: `CompleteMultipartUpload` либо успешно завершается (объект
|
|||
|
|
появляется атомарно), либо не удается (объект не создается). Отсутствует частичное состояние.
|
|||
|
|
|
|||
|
|
3. **Не требуется переименование**: Конечный ключ объекта указывается во время
|
|||
|
|
`CreateMultipartUpload`. Фрагменты объединяются непосредственно в этот ключ.
|
|||
|
|
|
|||
|
|
4. **Объединение на стороне сервера**: S3 объединяет фрагменты внутренне. Библиотекарь
|
|||
|
|
никогда не считывает фрагменты обратно - нулевые накладные расходы памяти независимо от размера документа.
|
|||
|
|
|
|||
|
|
### Расширения BlobStore
|
|||
|
|
|
|||
|
|
**Файл:** `trustgraph-flow/trustgraph/librarian/blob_store.py`
|
|||
|
|
|
|||
|
|
Добавлены методы многокомпонентной загрузки:
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
class BlobStore:
|
|||
|
|
# Existing methods...
|
|||
|
|
|
|||
|
|
def create_multipart_upload(self, object_id: UUID, kind: str) -> str:
|
|||
|
|
"""Initialize multipart upload, return s3_upload_id."""
|
|||
|
|
# minio client: create_multipart_upload()
|
|||
|
|
|
|||
|
|
def upload_part(
|
|||
|
|
self, object_id: UUID, s3_upload_id: str,
|
|||
|
|
part_number: int, data: bytes
|
|||
|
|
) -> str:
|
|||
|
|
"""Upload a single part, return etag."""
|
|||
|
|
# minio client: upload_part()
|
|||
|
|
# Note: S3 part numbers are 1-indexed
|
|||
|
|
|
|||
|
|
def complete_multipart_upload(
|
|||
|
|
self, object_id: UUID, s3_upload_id: str,
|
|||
|
|
parts: List[Tuple[int, str]] # [(part_number, etag), ...]
|
|||
|
|
) -> None:
|
|||
|
|
"""Finalize multipart upload."""
|
|||
|
|
# minio client: complete_multipart_upload()
|
|||
|
|
|
|||
|
|
def abort_multipart_upload(
|
|||
|
|
self, object_id: UUID, s3_upload_id: str
|
|||
|
|
) -> None:
|
|||
|
|
"""Cancel multipart upload, clean up parts."""
|
|||
|
|
# minio client: abort_multipart_upload()
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Особенности размера фрагментов
|
|||
|
|
|
|||
|
|
**Минимальный размер для S3**: 5 МБ на часть (кроме последней части)
|
|||
|
|
**Максимальный размер для S3**: 10 000 частей на загрузку
|
|||
|
|
**Рекомендуемый размер по умолчанию**: фрагменты по 5 МБ
|
|||
|
|
Документ объемом 500 МБ = 100 фрагментов
|
|||
|
|
Документ объемом 5 ГБ = 1000 фрагментов
|
|||
|
|
**Гранулярность прогресса**: Меньшие фрагменты = более точные обновления прогресса
|
|||
|
|
**Эффективность сети**: Большие фрагменты = меньше сетевых запросов
|
|||
|
|
|
|||
|
|
Размер фрагмента может быть настроен клиентом в пределах заданных ограничений (от 5 МБ до 100 МБ).
|
|||
|
|
|
|||
|
|
### Обработка документов: Потоковая загрузка
|
|||
|
|
|
|||
|
|
Процесс загрузки предназначен для эффективной передачи документов в хранилище. Процесс обработки предназначен для извлечения и разделения документов на фрагменты без полной загрузки
|
|||
|
|
их в память.
|
|||
|
|
|
|||
|
|
|
|||
|
|
#### Основной принцип: Идентификатор, а не содержимое
|
|||
|
|
|
|||
|
|
В настоящее время, при запуске обработки, содержимое документа передается через сообщения Pulsar. Это приводит к полной загрузке документов в память. Вместо этого:
|
|||
|
|
|
|||
|
|
|
|||
|
|
Сообщения Pulsar содержат только **идентификатор документа**
|
|||
|
|
Обработчики получают содержимое документа непосредственно из хранилища.
|
|||
|
|
Получение происходит в виде **потока во временный файл**
|
|||
|
|
Разбор документов, специфичный для каждого формата (PDF, текст и т.д.), работает с файлами, а не с буферами памяти.
|
|||
|
|
|
|||
|
|
Это позволяет хранилищу быть независимым от структуры документа. Разбор PDF, извлечение текста и другая логика, специфичная для формата, остается в соответствующих декодерах.
|
|||
|
|
|
|||
|
|
|
|||
|
|
#### Процесс обработки
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
Pulsar PDF Decoder Librarian S3
|
|||
|
|
│ │ │ │
|
|||
|
|
│── doc-id ───────────►│ │ │
|
|||
|
|
│ (processing msg) │ │ │
|
|||
|
|
│ │ │ │
|
|||
|
|
│ │── stream-document ──────►│ │
|
|||
|
|
│ │ (doc-id) │── GetObject ────►│
|
|||
|
|
│ │ │ │
|
|||
|
|
│ │◄── chunk ────────────────│◄── stream ───────│
|
|||
|
|
│ │ (write to temp file) │ │
|
|||
|
|
│ │◄── chunk ────────────────│◄── stream ───────│
|
|||
|
|
│ │ (append to temp file) │ │
|
|||
|
|
│ │ ⋮ │ ⋮ │
|
|||
|
|
│ │◄── EOF ──────────────────│ │
|
|||
|
|
│ │ │ │
|
|||
|
|
│ │ ┌──────────────────────────┐ │
|
|||
|
|
│ │ │ temp file on disk │ │
|
|||
|
|
│ │ │ (memory stays bounded) │ │
|
|||
|
|
│ │ └────────────┬─────────────┘ │
|
|||
|
|
│ │ │ │
|
|||
|
|
│ │ PDF library opens file │
|
|||
|
|
│ │ extract page 1 text ──► chunker │
|
|||
|
|
│ │ extract page 2 text ──► chunker │
|
|||
|
|
│ │ ⋮ │
|
|||
|
|
│ │ close file │
|
|||
|
|
│ │ delete temp file │
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### API потоковой передачи данных для библиотекаря
|
|||
|
|
|
|||
|
|
Добавить операцию получения документов в режиме потоковой передачи:
|
|||
|
|
|
|||
|
|
**`stream-document`**
|
|||
|
|
|
|||
|
|
Запрос:
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"operation": "stream-document",
|
|||
|
|
"document-id": "doc-123"
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Ответ: Потоковые двоичные фрагменты (не единый ответ).
|
|||
|
|
|
|||
|
|
Для REST API это возвращает потоковый ответ с `Transfer-Encoding: chunked`.
|
|||
|
|
|
|||
|
|
Для внутренних вызовов между сервисами (от процессора к библиотеке), это может быть:
|
|||
|
|
Прямая потоковая передача данных через S3 с использованием предварительно подписанного URL (если внутренняя сеть это позволяет).
|
|||
|
|
Фрагментированные ответы через протокол сервиса.
|
|||
|
|
Специальный конечный пункт для потоковой передачи данных.
|
|||
|
|
|
|||
|
|
Основное требование: данные передаются фрагментами, никогда полностью не буферизуются в библиотеке.
|
|||
|
|
|
|||
|
|
#### Изменения декодера PDF
|
|||
|
|
|
|||
|
|
**Текущая реализация** (требует много памяти):
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
def decode_pdf(document_content: bytes) -> str:
|
|||
|
|
reader = PdfReader(BytesIO(document_content)) # full doc in memory
|
|||
|
|
text = ""
|
|||
|
|
for page in reader.pages:
|
|||
|
|
text += page.extract_text() # accumulating
|
|||
|
|
return text # full text in memory
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Новая реализация** (временный файл, постепенная):
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
def decode_pdf_streaming(doc_id: str, librarian_client) -> Iterator[str]:
|
|||
|
|
"""Yield extracted text page by page."""
|
|||
|
|
|
|||
|
|
with tempfile.NamedTemporaryFile(delete=True, suffix='.pdf') as tmp:
|
|||
|
|
# Stream document to temp file
|
|||
|
|
for chunk in librarian_client.stream_document(doc_id):
|
|||
|
|
tmp.write(chunk)
|
|||
|
|
tmp.flush()
|
|||
|
|
|
|||
|
|
# Open PDF from file (not memory)
|
|||
|
|
reader = PdfReader(tmp.name)
|
|||
|
|
|
|||
|
|
# Yield pages incrementally
|
|||
|
|
for page in reader.pages:
|
|||
|
|
yield page.extract_text()
|
|||
|
|
|
|||
|
|
# tmp file auto-deleted on context exit
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Профиль использования памяти:
|
|||
|
|
Временный файл на диске: размер PDF (диск дешевый).
|
|||
|
|
В памяти: текст одной страницы за раз.
|
|||
|
|
Максимальный объем памяти: ограничен, не зависит от размера документа.
|
|||
|
|
|
|||
|
|
#### Изменения в декодере текстовых документов
|
|||
|
|
|
|||
|
|
Для обычных текстовых документов еще проще - временный файл не нужен:
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
def decode_text_streaming(doc_id: str, librarian_client) -> Iterator[str]:
|
|||
|
|
"""Yield text in chunks as it streams from storage."""
|
|||
|
|
|
|||
|
|
buffer = ""
|
|||
|
|
for chunk in librarian_client.stream_document(doc_id):
|
|||
|
|
buffer += chunk.decode('utf-8')
|
|||
|
|
|
|||
|
|
# Yield complete lines/paragraphs as they arrive
|
|||
|
|
while '\n\n' in buffer:
|
|||
|
|
paragraph, buffer = buffer.split('\n\n', 1)
|
|||
|
|
yield paragraph + '\n\n'
|
|||
|
|
|
|||
|
|
# Yield remaining buffer
|
|||
|
|
if buffer:
|
|||
|
|
yield buffer
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Текстовые документы могут передаваться напрямую без временного файла, поскольку они имеют
|
|||
|
|
линейную структуру.
|
|||
|
|
|
|||
|
|
#### Интеграция с модулем разбиения на части (Chunker)
|
|||
|
|
|
|||
|
|
Модуль разбиения на части (chunker) получает итератор текстовых данных (страниц или абзацев) и
|
|||
|
|
создает фрагменты постепенно:
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
class StreamingChunker:
|
|||
|
|
def __init__(self, chunk_size: int, overlap: int):
|
|||
|
|
self.chunk_size = chunk_size
|
|||
|
|
self.overlap = overlap
|
|||
|
|
|
|||
|
|
def process(self, text_stream: Iterator[str]) -> Iterator[str]:
|
|||
|
|
"""Yield chunks as text arrives."""
|
|||
|
|
buffer = ""
|
|||
|
|
|
|||
|
|
for text_segment in text_stream:
|
|||
|
|
buffer += text_segment
|
|||
|
|
|
|||
|
|
while len(buffer) >= self.chunk_size:
|
|||
|
|
chunk = buffer[:self.chunk_size]
|
|||
|
|
yield chunk
|
|||
|
|
# Keep overlap for context continuity
|
|||
|
|
buffer = buffer[self.chunk_size - self.overlap:]
|
|||
|
|
|
|||
|
|
# Yield remaining buffer as final chunk
|
|||
|
|
if buffer.strip():
|
|||
|
|
yield buffer
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### Конвейер сквозной обработки
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
async def process_document(doc_id: str, librarian_client, embedder):
|
|||
|
|
"""Process document with bounded memory."""
|
|||
|
|
|
|||
|
|
# Get document metadata to determine type
|
|||
|
|
metadata = await librarian_client.get_document_metadata(doc_id)
|
|||
|
|
|
|||
|
|
# Select decoder based on document type
|
|||
|
|
if metadata.kind == 'application/pdf':
|
|||
|
|
text_stream = decode_pdf_streaming(doc_id, librarian_client)
|
|||
|
|
elif metadata.kind == 'text/plain':
|
|||
|
|
text_stream = decode_text_streaming(doc_id, librarian_client)
|
|||
|
|
else:
|
|||
|
|
raise UnsupportedDocumentType(metadata.kind)
|
|||
|
|
|
|||
|
|
# Chunk incrementally
|
|||
|
|
chunker = StreamingChunker(chunk_size=1000, overlap=100)
|
|||
|
|
|
|||
|
|
# Process each chunk as it's produced
|
|||
|
|
for chunk in chunker.process(text_stream):
|
|||
|
|
# Generate embeddings, store in vector DB, etc.
|
|||
|
|
embedding = await embedder.embed(chunk)
|
|||
|
|
await store_chunk(doc_id, chunk, embedding)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
В любой момент времени полный документ или полностью извлеченный текст не хранятся в памяти.
|
|||
|
|
|
|||
|
|
#### Особенности использования временных файлов
|
|||
|
|
|
|||
|
|
**Расположение**: Используйте системную временную директорию (`/tmp` или эквивалент). Для
|
|||
|
|
контейнеризированных развертываний убедитесь, что временная директория имеет достаточно места
|
|||
|
|
и находится на быстром носителе (желательно не на сетевом диске).
|
|||
|
|
|
|||
|
|
**Очистка**: Используйте контекстные менеджеры (`with tempfile...`) для обеспечения очистки
|
|||
|
|
даже в случае возникновения исключений.
|
|||
|
|
|
|||
|
|
**Параллельная обработка**: Каждый процесс обработки получает свой временный файл.
|
|||
|
|
Отсутствуют конфликты между параллельной обработкой документов.
|
|||
|
|
|
|||
|
|
**Место на диске**: Временные файлы существуют недолго (продолжительность обработки). Для
|
|||
|
|
PDF-файла объемом 500 МБ требуется 500 МБ временного пространства во время обработки. Ограничение размера может
|
|||
|
|
быть применено во время загрузки, если место на диске ограничено.
|
|||
|
|
|
|||
|
|
### Унифицированный интерфейс обработки: Дочерние документы
|
|||
|
|
|
|||
|
|
Извлечение текста из PDF-файлов и обработка текстовых документов должны быть интегрированы в одну
|
|||
|
|
общую цепочку обработки (разделение на фрагменты → создание векторных представлений → хранение). Для достижения этого с
|
|||
|
|
использованием единого интерфейса "получение по ID", извлеченные текстовые фрагменты сохраняются обратно
|
|||
|
|
в систему как дочерние документы.
|
|||
|
|
|
|||
|
|
#### Поток обработки с использованием дочерних документов
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
PDF Document Text Document
|
|||
|
|
│ │
|
|||
|
|
▼ │
|
|||
|
|
pdf-extractor │
|
|||
|
|
│ │
|
|||
|
|
│ (stream PDF from librarian) │
|
|||
|
|
│ (extract page 1 text) │
|
|||
|
|
│ (store as child doc → librarian) │
|
|||
|
|
│ (extract page 2 text) │
|
|||
|
|
│ (store as child doc → librarian) │
|
|||
|
|
│ ⋮ │
|
|||
|
|
▼ ▼
|
|||
|
|
[child-doc-id, child-doc-id, ...] [doc-id]
|
|||
|
|
│ │
|
|||
|
|
└─────────────────────┬───────────────────────────────┘
|
|||
|
|
▼
|
|||
|
|
chunker
|
|||
|
|
│
|
|||
|
|
│ (receives document ID)
|
|||
|
|
│ (streams content from librarian)
|
|||
|
|
│ (chunks incrementally)
|
|||
|
|
▼
|
|||
|
|
[chunks → embedding → storage]
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Модуль разбиения на фрагменты имеет один унифицированный интерфейс:
|
|||
|
|
Получение идентификатора документа (через Pulsar)
|
|||
|
|
Получение содержимого от хранилища данных
|
|||
|
|
Разбиение на фрагменты
|
|||
|
|
|
|||
|
|
Он не знает и не заботится о том, относится ли идентификатор к:
|
|||
|
|
Текстовому документу, загруженному пользователем
|
|||
|
|
Извлеченному текстовому фрагменту из страницы PDF
|
|||
|
|
Любому будущему типу документов
|
|||
|
|
|
|||
|
|
#### Метаданные дочернего документа
|
|||
|
|
|
|||
|
|
Расширьте схему документа для отслеживания отношений родитель/дочерний элемент:
|
|||
|
|
|
|||
|
|
```sql
|
|||
|
|
-- Add columns to document table
|
|||
|
|
ALTER TABLE document ADD parent_id text;
|
|||
|
|
ALTER TABLE document ADD document_type text;
|
|||
|
|
|
|||
|
|
-- Index for finding children of a parent
|
|||
|
|
CREATE INDEX document_parent ON document (parent_id);
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Типы документов:**
|
|||
|
|
|
|||
|
|
| `document_type` | Описание |
|
|||
|
|
|-----------------|-------------|
|
|||
|
|
| `source` | Документ, загруженный пользователем (PDF, текст и т.д.) |
|
|||
|
|
| `extracted` | Полученный из исходного документа (например, текст страницы PDF) |
|
|||
|
|
|
|||
|
|
**Поля метаданных:**
|
|||
|
|
|
|||
|
|
| Поле | Исходный документ | Извлеченный дочерний элемент |
|
|||
|
|
|-------|-----------------|-----------------|
|
|||
|
|
| `id` | Предоставлено пользователем или сгенерировано | сгенерировано (например, `{parent-id}-page-{n}`) |
|
|||
|
|
| `parent_id` | `NULL` | Идентификатор родительского документа |
|
|||
|
|
| `document_type` | `source` | `extracted` |
|
|||
|
|
| `kind` | `application/pdf` и т.д. | `text/plain` |
|
|||
|
|
| `title` | Предоставлено пользователем | сгенерировано (например, "Страница 3 отчета Report.pdf") |
|
|||
|
|
| `user` | Аутентифицированный пользователь | то же, что и у родительского элемента |
|
|||
|
|
|
|||
|
|
#### API для дочерних документов
|
|||
|
|
|
|||
|
|
**Создание дочерних документов** (внутреннее, используется pdf-extractor):
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"operation": "add-child-document",
|
|||
|
|
"parent-id": "doc-123",
|
|||
|
|
"document-metadata": {
|
|||
|
|
"id": "doc-123-page-1",
|
|||
|
|
"kind": "text/plain",
|
|||
|
|
"title": "Page 1"
|
|||
|
|
},
|
|||
|
|
"content": "<base64-encoded-text>"
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Для небольших извлеченных фрагментов текста (обычный текст страницы обычно менее 100 КБ) однокрасная загрузка является приемлемой. Для очень больших извлечений текста можно использовать загрузку по частям.
|
|||
|
|
|
|||
|
|
**Список дочерних документов** (для отладки/администрирования):
|
|||
|
|
|
|||
|
|
**Отображение дочерних документов** (для отладки/администрирования):
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"operation": "list-children",
|
|||
|
|
"parent-id": "doc-123"
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Ответ:
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"children": [
|
|||
|
|
{ "id": "doc-123-page-1", "title": "Page 1", "kind": "text/plain" },
|
|||
|
|
{ "id": "doc-123-page-2", "title": "Page 2", "kind": "text/plain" },
|
|||
|
|
...
|
|||
|
|
]
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### Поведение, видимое пользователю
|
|||
|
|
|
|||
|
|
**`list-documents` поведение по умолчанию:**
|
|||
|
|
|
|||
|
|
```sql
|
|||
|
|
SELECT * FROM document WHERE user = ? AND parent_id IS NULL;
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Только документы верхнего уровня отображаются в списке документов пользователя.
|
|||
|
|
Дочерние документы по умолчанию отфильтровываются.
|
|||
|
|
|
|||
|
|
**Необязательный флаг `include-children`** (для администраторов/отладки):
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"operation": "list-documents",
|
|||
|
|
"include-children": true
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### Каскадное удаление
|
|||
|
|
|
|||
|
|
При удалении родительского документа все дочерние элементы должны быть удалены:
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
def delete_document(doc_id: str):
|
|||
|
|
# Find all children
|
|||
|
|
children = query("SELECT id, object_id FROM document WHERE parent_id = ?", doc_id)
|
|||
|
|
|
|||
|
|
# Delete child blobs from S3
|
|||
|
|
for child in children:
|
|||
|
|
blob_store.delete(child.object_id)
|
|||
|
|
|
|||
|
|
# Delete child metadata from Cassandra
|
|||
|
|
execute("DELETE FROM document WHERE parent_id = ?", doc_id)
|
|||
|
|
|
|||
|
|
# Delete parent blob and metadata
|
|||
|
|
parent = get_document(doc_id)
|
|||
|
|
blob_store.delete(parent.object_id)
|
|||
|
|
execute("DELETE FROM document WHERE id = ? AND user = ?", doc_id, user)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### Соображения по поводу хранения
|
|||
|
|
|
|||
|
|
Извлеченные текстовые фрагменты дублируют контент:
|
|||
|
|
Оригинальный PDF-файл хранится в хранилище "Garage".
|
|||
|
|
Извлеченный текст для каждой страницы также хранится в хранилище "Garage".
|
|||
|
|
|
|||
|
|
Этот компромисс позволяет:
|
|||
|
|
**Единый интерфейс для разбиения на фрагменты**: Разбиватель всегда получает данные по идентификатору.
|
|||
|
|
**Возобновление/повтор**: Можно перезапустить на этапе разбиения на фрагменты без повторной извлечения PDF-файла.
|
|||
|
|
**Отладка**: Извлеченный текст можно просмотреть.
|
|||
|
|
**Разделение ответственности**: Сервисы извлечения PDF и разбиения на фрагменты являются независимыми.
|
|||
|
|
|
|||
|
|
Для PDF-файла объемом 500 МБ с 200 страницами, в среднем 5 КБ текста на страницу:
|
|||
|
|
Хранение PDF: 500 МБ.
|
|||
|
|
Хранение извлеченного текста: около 1 МБ в общей сложности.
|
|||
|
|
Дополнительные затраты: незначительные.
|
|||
|
|
|
|||
|
|
#### Вывод извлечения текста из PDF-файла
|
|||
|
|
|
|||
|
|
Сервис извлечения текста из PDF-файла, после обработки документа:
|
|||
|
|
|
|||
|
|
1. Получает PDF-файл из хранилища "librarian" во временный файл.
|
|||
|
|
2. Извлекает текст страницу за страницей.
|
|||
|
|
3. Для каждой страницы сохраняет извлеченный текст как дочерний документ через хранилище "librarian".
|
|||
|
|
4. Отправляет идентификаторы дочерних документов в очередь разбивателя на фрагменты.
|
|||
|
|
Выходной документ.
|
|||
|
|
```python
|
|||
|
|
async def extract_pdf(doc_id: str, librarian_client, output_queue):
|
|||
|
|
"""Extract PDF pages and store as child documents."""
|
|||
|
|
|
|||
|
|
with tempfile.NamedTemporaryFile(delete=True, suffix='.pdf') as tmp:
|
|||
|
|
# Stream PDF to temp file
|
|||
|
|
for chunk in librarian_client.stream_document(doc_id):
|
|||
|
|
tmp.write(chunk)
|
|||
|
|
tmp.flush()
|
|||
|
|
|
|||
|
|
# Extract pages
|
|||
|
|
reader = PdfReader(tmp.name)
|
|||
|
|
for page_num, page in enumerate(reader.pages, start=1):
|
|||
|
|
text = page.extract_text()
|
|||
|
|
|
|||
|
|
# Store as child document
|
|||
|
|
child_id = f"{doc_id}-page-{page_num}"
|
|||
|
|
await librarian_client.add_child_document(
|
|||
|
|
parent_id=doc_id,
|
|||
|
|
document_id=child_id,
|
|||
|
|
kind="text/plain",
|
|||
|
|
title=f"Page {page_num}",
|
|||
|
|
content=text.encode('utf-8')
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# Send to chunker queue
|
|||
|
|
await output_queue.send(child_id)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Модуль, отвечающий за разделение на части, получает эти идентификаторы дочерних элементов и обрабатывает их так же, как он бы обрабатывал текстовый документ, загруженный пользователем.
|
|||
|
|
|
|||
|
|
### Обновления для клиентов
|
|||
|
|
|
|||
|
|
#### Python SDK
|
|||
|
|
|
|||
|
|
|
|||
|
|
Python SDK (`trustgraph-base/trustgraph/api/library.py`) должен обрабатывать
|
|||
|
|
загрузку данных, разделенных на части, прозрачно. Публичный интерфейс остается неизменным:
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
# Existing interface - no change for users
|
|||
|
|
library.add_document(
|
|||
|
|
id="doc-123",
|
|||
|
|
title="Large Report",
|
|||
|
|
kind="application/pdf",
|
|||
|
|
content=large_pdf_bytes, # Can be hundreds of MB
|
|||
|
|
tags=["reports"]
|
|||
|
|
)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Внутри, SDK определяет размер документа и переключает стратегию:
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
class Library:
|
|||
|
|
CHUNKED_UPLOAD_THRESHOLD = 2 * 1024 * 1024 # 2MB
|
|||
|
|
|
|||
|
|
def add_document(self, id, title, kind, content, tags=None, ...):
|
|||
|
|
if len(content) < self.CHUNKED_UPLOAD_THRESHOLD:
|
|||
|
|
# Small document: single operation (existing behavior)
|
|||
|
|
return self._add_document_single(id, title, kind, content, tags)
|
|||
|
|
else:
|
|||
|
|
# Large document: chunked upload
|
|||
|
|
return self._add_document_chunked(id, title, kind, content, tags)
|
|||
|
|
|
|||
|
|
def _add_document_chunked(self, id, title, kind, content, tags):
|
|||
|
|
# 1. begin-upload
|
|||
|
|
session = self._begin_upload(
|
|||
|
|
document_metadata={...},
|
|||
|
|
total_size=len(content),
|
|||
|
|
chunk_size=5 * 1024 * 1024
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# 2. upload-chunk for each chunk
|
|||
|
|
for i, chunk in enumerate(self._chunk_bytes(content, session.chunk_size)):
|
|||
|
|
self._upload_chunk(session.upload_id, i, chunk)
|
|||
|
|
|
|||
|
|
# 3. complete-upload
|
|||
|
|
return self._complete_upload(session.upload_id)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Функции обратного вызова для отслеживания прогресса** (необязательное улучшение):
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
def add_document(self, ..., on_progress=None):
|
|||
|
|
"""
|
|||
|
|
on_progress: Optional callback(bytes_sent, total_bytes)
|
|||
|
|
"""
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Это позволяет пользовательским интерфейсам отображать ход загрузки без изменения основного API.
|
|||
|
|
|
|||
|
|
#### Инструменты командной строки
|
|||
|
|
|
|||
|
|
**`tg-add-library-document`** продолжает работать без изменений:
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
# Works transparently for any size - SDK handles chunking internally
|
|||
|
|
tg-add-library-document --file large-report.pdf --title "Large Report"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Возможно, можно добавить опциональное отображение прогресса:
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
tg-add-library-document --file large-report.pdf --title "Large Report" --progress
|
|||
|
|
# Output:
|
|||
|
|
# Uploading: 45% (225MB / 500MB)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Устаревшие инструменты удалены:**
|
|||
|
|
|
|||
|
|
`tg-load-pdf` - устарело, используйте `tg-add-library-document`
|
|||
|
|
`tg-load-text` - устарело, используйте `tg-add-library-document`
|
|||
|
|
|
|||
|
|
**Команды администратора/отладки** (опционально, низкий приоритет):
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
# List incomplete uploads (admin troubleshooting)
|
|||
|
|
tg-add-library-document --list-pending
|
|||
|
|
|
|||
|
|
# Resume specific upload (recovery scenario)
|
|||
|
|
tg-add-library-document --resume upload-abc-123 --file large-report.pdf
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Это могут быть флаги для существующей команды, а не отдельные инструменты.
|
|||
|
|
|
|||
|
|
#### Обновления спецификации API
|
|||
|
|
|
|||
|
|
Спецификация OpenAPI (`specs/api/paths/librarian.yaml`) требует обновлений для:
|
|||
|
|
|
|||
|
|
**Новые операции:**
|
|||
|
|
|
|||
|
|
`begin-upload` - Инициализация сессии загрузки по частям
|
|||
|
|
`upload-chunk` - Загрузка отдельного фрагмента
|
|||
|
|
`complete-upload` - Завершение загрузки
|
|||
|
|
`abort-upload` - Отмена загрузки
|
|||
|
|
`get-upload-status` - Запрос хода загрузки
|
|||
|
|
`list-uploads` - Список незавершенных загрузок для пользователя
|
|||
|
|
`stream-document` - Получение документа в режиме потоковой передачи
|
|||
|
|
`add-child-document` - Сохранение извлеченного текста (внутреннее использование)
|
|||
|
|
`list-children` - Список дочерних документов (для администраторов)
|
|||
|
|
|
|||
|
|
**Измененные операции:**
|
|||
|
|
|
|||
|
|
`list-documents` - Добавлен параметр `include-children`
|
|||
|
|
|
|||
|
|
**Новые схемы:**
|
|||
|
|
|
|||
|
|
`ChunkedUploadBeginRequest`
|
|||
|
|
`ChunkedUploadBeginResponse`
|
|||
|
|
`ChunkedUploadChunkRequest`
|
|||
|
|
`ChunkedUploadChunkResponse`
|
|||
|
|
`UploadSession`
|
|||
|
|
`UploadProgress`
|
|||
|
|
|
|||
|
|
**Обновления спецификации WebSocket** (`specs/websocket/`):
|
|||
|
|
|
|||
|
|
Отразите операции REST для клиентов WebSocket, обеспечивая обновления хода загрузки в режиме реального времени.
|
|||
|
|
|
|||
|
|
|
|||
|
|
#### Соображения пользовательского интерфейса
|
|||
|
|
|
|||
|
|
Обновления спецификации API позволяют улучшить пользовательский интерфейс:
|
|||
|
|
|
|||
|
|
**Интерфейс отображения хода загрузки:**
|
|||
|
|
Индикатор прогресса, показывающий загруженные фрагменты
|
|||
|
|
Оценка оставшегося времени
|
|||
|
|
Возможность приостановки/возобновления
|
|||
|
|
|
|||
|
|
**Восстановление после ошибок:**
|
|||
|
|
Опция "Возобновить загрузку" для прерванных загрузок
|
|||
|
|
Список ожидающих загрузок при повторном подключении
|
|||
|
|
|
|||
|
|
**Обработка больших файлов:**
|
|||
|
|
Обнаружение размера файла на стороне клиента
|
|||
|
|
Автоматическая загрузка по частям для больших файлов
|
|||
|
|
Четкая обратная связь во время длительных загрузок
|
|||
|
|
|
|||
|
|
Эти улучшения пользовательского интерфейса требуют работы на стороне клиентской части, основанной на обновленной спецификации API.
|