mirror of
https://github.com/trustgraph-ai/trustgraph.git
synced 2026-04-25 16:36:21 +02:00
Structure the tech specs directory (#836)
Tech spec some subdirectories for different languages
This commit is contained in:
parent
48da6c5f8b
commit
e7efb673ef
423 changed files with 0 additions and 0 deletions
|
|
@ -1,992 +0,0 @@
|
|||
---
|
||||
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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue