Native CLI i18n: The TrustGraph CLI has built-in translation support that dynamically loads language strings. You can test and use different languages by simply passing the --lang flag (e.g., --lang es for Spanish, --lang ru for Russian) or by configuring your environment's LANG variable. Automated Docs Translations: This PR introduces autonomously translated Markdown documentation into several target languages, including Spanish, Swahili, Portuguese, Turkish, Hindi, Hebrew, Arabic, Simplified Chinese, and Russian.
40 KiB
| layout | title | parent |
|---|---|---|
| default | Especificación Técnica de Carga de Documentos Grandes | Spanish (Beta) |
Especificación Técnica de Carga de Documentos Grandes
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.
Resumen
Esta especificación aborda los problemas de escalabilidad y la experiencia del usuario al cargar documentos grandes en TrustGraph. La arquitectura actual trata la carga de documentos como una operación atómica única, lo que provoca una alta carga de memoria en varios puntos del proceso y no proporciona ninguna indicación de progreso ni opciones de recuperación a los usuarios.
Esta implementación tiene como objetivo los siguientes casos de uso:
- Procesamiento de PDF Grandes: Cargar y procesar archivos PDF de varios cientos de megabytes sin agotar la memoria.
- Cargas Reanudables: Permitir que las cargas interrumpidas continúen desde donde se detuvieron en lugar de reiniciarse. <<<<<<< HEAD
- Indicación de Progreso: Proporcionar a los usuarios visibilidad en tiempo real del =======
- Retroalimentación de Progreso: Proporcionar a los usuarios visibilidad en tiempo real del
82edf2d (New md files from RunPod) progreso de la carga y el procesamiento.
- Procesamiento Eficiente en Memoria: Procesar documentos de forma continua sin mantener archivos completos en la memoria.
Objetivos
<<<<<<< HEAD Carga Incremental: Soporte para la carga de documentos en fragmentos a través de REST y WebSocket. Transferencias Reanudables: Permitir la recuperación de cargas interrumpidas. Visibilidad del Progreso: Proporcionar retroalimentación de carga/procesamiento a los clientes. Eficiencia de Memoria: Eliminar el almacenamiento en búfer de documentos completos en todo el proceso. Compatibilidad con Versiones Anteriores: Los flujos de trabajo existentes para documentos pequeños continúan sin cambios. Procesamiento por Flujo Continuo: La decodificación de PDF y el fragmentado de texto operan en flujos.
Carga Incremental: Soporte para la carga de documentos por partes a través de REST y WebSocket. Transferencias Reanudables: Permitir la recuperación de cargas interrumpidas. Visibilidad del Progreso: Proporcionar retroalimentación de progreso de carga/procesamiento a los clientes. Eficiencia de Memoria: Eliminar el almacenamiento en búfer de documentos completos en todo el proceso. Compatibilidad con Versiones Anteriores: Los flujos de trabajo existentes para documentos pequeños continúan sin cambios. Procesamiento por Transmisión: La decodificación de PDF y el fragmentado de texto operan en flujos.
82edf2d (New md files from RunPod)
Antecedentes
Arquitectura Actual
El flujo de envío de documentos sigue la siguiente ruta:
- El cliente envía el documento a través de REST (
POST /api/v1/librarian) o WebSocket. - La API Gateway recibe la solicitud completa con el contenido del documento codificado en base64.
- El LibrarianRequestor traduce la solicitud a un mensaje Pulsar.
- El Librarian Service recibe el mensaje, decodifica el documento en la memoria.
- BlobStore carga el documento en Garage/S3.
- Cassandra almacena los metadatos con la referencia del objeto. <<<<<<< HEAD
- Para el procesamiento: el documento se recupera de S3, se decodifica y se divide en fragmentos, todo en la memoria. =======
- Para el procesamiento: el documento se recupera de S3, se decodifica y se divide en partes, todo en la memoria.
82edf2d (New md files from RunPod)
Archivos clave:
Punto de entrada REST/WebSocket: trustgraph-flow/trustgraph/gateway/service.py
Núcleo de Librarian: trustgraph-flow/trustgraph/librarian/librarian.py
Almacenamiento de blobs: trustgraph-flow/trustgraph/librarian/blob_store.py
Tablas de Cassandra: trustgraph-flow/trustgraph/tables/library.py
Esquema de la API: trustgraph-base/trustgraph/schema/services/library.py
Limitaciones Actuales
<<<<<<< HEAD El diseño actual tiene varios problemas de memoria y experiencia de usuario que se agravan:
El diseño actual tiene varios problemas de memoria y experiencia de usuario:
82edf2d (New md files from RunPod)
-
Operación de Carga Atómica: Se debe transmitir todo el documento en una solicitud única. Los documentos grandes requieren solicitudes de larga duración sin indicación de progreso ni mecanismo de reintento si la conexión falla.
-
Diseño de la API: Tanto las API REST como WebSocket esperan el documento completo en un solo mensaje. El esquema (
LibrarianRequest) tiene un campocontentque contiene todo el documento codificado en base64. -
Memoria del Librarian: El servicio librarian decodifica todo el documento <<<<<<< HEAD en la memoria antes de cargarlo en S3. Para un PDF de 500 MB, esto significa mantener 500 MB+ en la memoria del proceso.
-
Memoria del Decodificador de PDF: Cuando comienza el procesamiento, el decodificador de PDF carga todo el PDF en la memoria para extraer el texto. Las bibliotecas como PyPDF y similares típicamente requieren acceso a todo el documento. ======= en la memoria antes de cargarlo en S3. Para un archivo PDF de 500 MB, esto significa mantener 500 MB+ en la memoria del proceso.
-
Memoria del Decodificador de PDF: Cuando comienza el procesamiento, el decodificador de PDF carga todo el PDF en la memoria para extraer el texto. Las bibliotecas como PyPDF normalmente requieren acceso a todo el documento.
82edf2d (New md files from RunPod)
- Memoria del Fragmentador: El fragmentador de texto recibe todo el texto extraído y lo mantiene en la memoria mientras produce fragmentos.
Ejemplo de Impacto en la Memoria (PDF de 500 MB): Gateway: ~700 MB (sobrecarga de codificación base64) Librarian: ~500 MB (bytes decodificados) Decodificador de PDF: ~500 MB + búferes de extracción Fragmentador: texto extraído (variable, potencialmente 100 MB+)
El pico total de memoria puede exceder los 2 GB para un solo documento grande.
Diseño Técnico
Principios de Diseño
-
Fachada de la API: Toda la interacción del cliente pasa por la API de librarian. Los clientes no tienen acceso directo ni conocimiento del almacenamiento subyacente de S3/Garage.
-
Carga Multipart de S3: Utilice la carga multipart estándar de S3. Esto está ampliamente soportado en sistemas compatibles con S3 (AWS S3, MinIO, Garage, Ceph, DigitalOcean Spaces, Backblaze B2, etc.), lo que garantiza la portabilidad.
-
Completación Atómica: Las cargas multipart de S3 son inherentemente atómicas: las partes cargadas <<<<<<< HEAD son invisibles hasta que se llama a
CompleteMultipartUpload. No se necesitan archivos temporales ni ======= no son visibles hasta que se llama aCompleteMultipartUpload. No se necesitan archivos temporales ni
82edf2d (New md files from RunPod) operaciones de renombrado.
- Estado Rastreable: Las sesiones de carga se rastrean en Cassandra, lo que proporciona visibilidad de las cargas incompletas y permite la capacidad de reanudación.
<<<<<<< HEAD
Flujo de Carga Fragmentada
=======
Flujo de Carga por Partes
82edf2d (New md files from RunPod)
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) │
El cliente nunca interactúa directamente con S3. El "librarian" (bibliotecario) traduce entre nuestra API de carga por partes y las operaciones multipart de S3 internamente.
Operaciones de la API del "Librarian"
begin-upload
Inicializar una sesión de carga por partes.
Solicitud:
{
"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
}
Respuesta:
{
"upload-id": "upload-abc-123",
"chunk-size": 5242880,
"total-chunks": 100
}
El bibliotecario: <<<<<<< HEAD
- Genera un
upload_idy unobject_idúnicos (UUID para almacenamiento de blobs). ======= - Genera un
upload_idy unobject_idúnicos (UUID para el almacenamiento de blobs).
82edf2d (New md files from RunPod)
- Llama a S3
CreateMultipartUpload, recibes3_upload_id. - Crea un registro de sesión en Cassandra.
- Devuelve
upload_idal cliente.
upload-chunk
<<<<<<< HEAD Cargar un único fragmento.
Carga un único fragmento.
82edf2d (New md files from RunPod)
Solicitud:
{
"operation": "upload-chunk",
"upload-id": "upload-abc-123",
"chunk-index": 0,
"content": "<base64-encoded-chunk>"
}
Respuesta:
{
"upload-id": "upload-abc-123",
"chunk-index": 0,
"chunks-received": 1,
"total-chunks": 100,
"bytes-received": 5242880,
"total-bytes": 524288000
}
El bibliotecario:
- Busca la sesión por
upload_id - Valida la propiedad (el usuario debe coincidir con el creador de la sesión)
- Llama a S3
UploadPartcon los datos del fragmento, recibeetag - Actualiza el registro de la sesión con el índice del fragmento y la etiqueta (etag)
- Devuelve el progreso al cliente
<<<<<<< HEAD
Los fragmentos fallidos se pueden reintentar; simplemente envía el mismo chunk-index nuevamente.
Los fragmentos fallidos se pueden reintentar: simplemente envía el mismo chunk-index nuevamente.
82edf2d (New md files from RunPod)
complete-upload
Finaliza la carga y crea el documento.
Solicitud:
{
"operation": "complete-upload",
"upload-id": "upload-abc-123"
}
Respuesta:
{
"document-id": "doc-123",
"object-id": "550e8400-e29b-41d4-a716-446655440000"
}
El bibliotecario:
- Busca la sesión, verifica que se hayan recibido todos los fragmentos.
- Llama a S3
CompleteMultipartUploadcon los ETags de las partes (S3 combina las partes <<<<<<< HEAD internamente, sin costo de memoria para el bibliotecario). ======= internamente, lo que no tiene costo de memoria para el bibliotecario).
82edf2d (New md files from RunPod)
- Crea un registro de documento en Cassandra con metadatos y referencia al objeto.
- Elimina el registro de la sesión de carga.
- Devuelve el ID del documento al cliente.
abort-upload
Cancelar una carga en curso.
Solicitud:
{
"operation": "abort-upload",
"upload-id": "upload-abc-123"
}
El bibliotecario:
- Llama a S3
AbortMultipartUploadpara limpiar partes. - Elimina el registro de sesión de Cassandra.
get-upload-status
Consulta el estado de una carga (para la capacidad de reanudación).
Solicitud:
{
"operation": "get-upload-status",
"upload-id": "upload-abc-123"
}
Respuesta:
{
"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
Listar las subidas incompletas para un usuario.
Solicitud:
{
"operation": "list-uploads"
}
Respuesta:
{
"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"
}
]
}
Almacenamiento de Sesión de Carga
Realizar un seguimiento de las cargas en curso en Cassandra:
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);
Comportamiento de TTL: Las sesiones expiran después de 24 horas si no se completan. Cuando expira el TTL de Cassandra, se elimina el registro de la sesión. Las partes de S3 huérfanas se eliminan mediante la política de ciclo de vida de S3 (configurar en el bucket).
Manejo de errores y atomicidad
Fallo en la carga de fragmentos:
<<<<<<< HEAD
El cliente reintenta el fragmento fallido (mismo upload_id y chunk-index).
El cliente reintenta el fragmento fallido (con el mismo upload_id y chunk-index).
82edf2d (New md files from RunPod)
UploadPartde S3 es idempotente para el mismo número de parte. La sesión realiza un seguimiento de qué fragmentos tuvieron éxito.
Desconexión del cliente durante la carga:
La sesión permanece en Cassandra con los fragmentos recibidos registrados.
El cliente puede llamar a get-upload-status para ver qué falta.
Reanudar cargando solo los fragmentos faltantes, luego complete-upload.
Fallo en la carga completa:
CompleteMultipartUpload de S3 es atómico: o tiene éxito por completo o falla.
En caso de fallo, las partes permanecen y el cliente puede reintentar complete-upload.
Nunca se muestra un documento parcial.
Vencimiento de la sesión: El TTL de Cassandra elimina el registro de la sesión después de 24 horas. <<<<<<< HEAD La política de ciclo de vida del bucket de S3 limpia las cargas multipartes incompletas. No se requiere limpieza manual.
Atomicidad de las cargas multipartes de S3
Las cargas multipartes de S3 proporcionan atomicidad integrada:
- Las partes son invisibles: Las partes cargadas no se pueden acceder como objetos. Solo existen como partes de una carga multipartes incompleta. ======= La política de ciclo de vida del bucket de S3 limpia las cargas multipart incompletas. No se requiere limpieza manual.
Atomicidad de las cargas multipart de S3
Las cargas multipart de S3 proporcionan atomicidad integrada:
- Las partes son invisibles: Las partes cargadas no se pueden acceder como objetos. Solo existen como partes de una carga multipart incompleta.
82edf2d (New md files from RunPod)
- Finalización atómica:
CompleteMultipartUploadtiene éxito (el objeto aparece de forma atómica) o falla (no se crea ningún objeto). No hay estado parcial.
<<<<<<< HEAD
3. No se necesita renombrar: La clave de objeto final se especifica en
el momento de CreateMultipartUpload. Las partes se combinan directamente en esa clave.
- No se necesita renombrar: La clave del objeto final se especifica en
el momento de
CreateMultipartUpload. Las partes se combinan directamente con esa clave.
82edf2d (New md files from RunPod)
- Combinación del lado del servidor: S3 combina las partes internamente. El bibliotecario nunca lee las partes de nuevo: cero sobrecarga de memoria independientemente del tamaño del documento.
Extensiones de BlobStore
Archivo: trustgraph-flow/trustgraph/librarian/blob_store.py
<<<<<<< HEAD Agregar métodos de carga multipartes:
Agregar métodos de carga multipart:
82edf2d (New md files from RunPod)
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()
Consideraciones sobre el tamaño de los bloques
Mínimo de S3: 5 MB por parte (excepto la última parte) Máximo de S3: 10,000 partes por carga Valor predeterminado práctico: bloques de 5 MB Documento de 500 MB = 100 bloques Documento de 5 GB = 1000 bloques Granularidad del progreso: Bloques más pequeños = actualizaciones de progreso más detalladas Eficiencia de la red: Bloques más grandes = menos viajes de ida y vuelta
El tamaño del bloque podría ser configurable por el cliente dentro de un rango (5 MB - 100 MB).
Procesamiento de documentos: Recuperación en streaming
El flujo de carga se ocupa de almacenar documentos de manera eficiente. El flujo de procesamiento se ocupa de extraer y dividir documentos sin cargarlos por completo en la memoria.
Principio de diseño: Identificador, no contenido
Actualmente, cuando se inicia el procesamiento, el contenido del documento fluye a través de mensajes de Pulsar. Esto carga documentos completos en la memoria. En cambio:
Los mensajes de Pulsar solo contienen el identificador del documento Los procesadores recuperan el contenido del documento directamente de la biblioteca. La recuperación se realiza como un flujo a un archivo temporal El análisis específico del documento (PDF, texto, etc.) funciona con archivos, no con búferes de memoria.
Esto mantiene a la biblioteca independiente de la estructura del documento. El análisis de PDF, la extracción de texto y otras lógicas específicas del formato permanecen en los decodificadores correspondientes.
Flujo de procesamiento
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 de flujo de trabajo del bibliotecario
Agregar una operación de recuperación de documentos en flujo continuo:
stream-document
Solicitud:
{
"operation": "stream-document",
"document-id": "doc-123"
}
Respuesta: Fragmentos binarios transmitidos (no una respuesta única).
Para la API REST, esto devuelve una respuesta transmitida con Transfer-Encoding: chunked.
Para llamadas internas de servicio a servicio (del procesador al bibliotecario), esto podría ser: Transmisión directa de S3 a través de una URL prefirmada (si la red interna lo permite). Respuestas fragmentadas a través del protocolo del servicio. Un punto final de transmisión dedicado.
El requisito clave: los datos fluyen en fragmentos, nunca completamente almacenados en búfer en el bibliotecario.
Cambios en el decodificador de PDF
Implementación actual (que consume mucha memoria):
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
Nueva implementación (archivo temporal, incremental):
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
Perfil de memoria: Archivo temporal en disco: tamaño del PDF (el disco es barato). En memoria: una página de texto a la vez. Memoria máxima: limitada, independiente del tamaño del documento.
Cambios en el decodificador de documentos de texto.
Para documentos de texto plano, aún más simple: no se necesita archivo temporal.
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
Los documentos de texto pueden transmitirse directamente sin un archivo temporal, ya que están estructurados linealmente.
Integración del Fragmentador (Chunker)
El fragmentador recibe un iterador de texto (páginas o párrafos) y produce fragmentos de forma incremental:
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
<<<<<<< HEAD
Canalización de procesamiento de extremo a extremo
=======
Canalización de Procesamiento de Extremo a Extremo
82edf2d (New md files from RunPod)
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)
<<<<<<< HEAD En ningún momento, se mantiene en memoria el documento completo o el texto extraído completo.
En ningún momento, el documento completo o el texto extraído se mantienen en la memoria.
82edf2d (New md files from RunPod)
Consideraciones sobre archivos temporales
Ubicación: Utilice el directorio temporal del sistema (/tmp o equivalente). Para
implementaciones en contenedores, asegúrese de que el directorio temporal tenga suficiente espacio
y que esté en un almacenamiento rápido (no montado en red, si es posible).
Limpieza: Utilice administradores de contexto (with tempfile...) para garantizar la limpieza
incluso en caso de excepciones.
Procesamiento concurrente: Cada trabajo de procesamiento obtiene su propio archivo temporal. No hay conflictos entre el procesamiento paralelo de documentos.
Espacio en disco: Los archivos temporales son de corta duración (duración del procesamiento). Para un archivo PDF de 500 MB, se necesitan 500 MB de espacio temporal durante el procesamiento. El límite de tamaño podría aplicarse en el momento de la carga si el espacio en disco es limitado.
Interfaz de procesamiento unificada: Documentos secundarios
La extracción de PDF y el procesamiento de documentos de texto deben integrarse en la misma canalización descendente (fragmentación → incrustaciones → almacenamiento). Para lograr esto con una interfaz "recuperación por ID" consistente, los bloques de texto extraídos se almacenan de nuevo en el sistema de gestión de documentos como documentos secundarios.
Flujo de procesamiento con documentos secundarios
Después de la última línea traducida, incluir una línea final que contenga exactamente: [[END_OF_TRANSLATION]]
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]
El componente de segmentación tiene una interfaz uniforme: Recibir un ID de documento (a través de Pulsar) Obtener el contenido del bibliotecario Segmentarlo
No sabe ni le importa si el ID se refiere a: Un documento de texto subido por un usuario Un fragmento de texto extraído de una página de PDF Cualquier tipo de documento futuro
Metadatos del Documento Hijo
Extender el esquema del documento para rastrear las relaciones padre/hijo:
-- 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);
Tipos de documentos:
document_type |
Descripción |
|---|---|
source |
Documento subido por el usuario (PDF, texto, etc.) |
extracted |
Derivado de un documento fuente (por ejemplo, texto de una página PDF) |
Campos de metadatos:
| Campo | Documento fuente | Documento hijo extraído |
|---|---|---|
id |
proporcionado por el usuario o generado | generado (por ejemplo, {parent-id}-page-{n}) |
parent_id |
NULL |
ID del documento padre |
document_type |
source |
extracted |
kind |
application/pdf, etc. |
text/plain |
title |
proporcionado por el usuario | generado (por ejemplo, "Página 3 del Informe.pdf") |
user |
usuario autenticado | igual que el padre |
<<<<<<< HEAD
API de Librarian para documentos hijos
Creación de documentos hijos (interno, utilizado por pdf-extractor):
API de Librarian para documentos hijo
Creación de documentos hijo (interno, utilizado por pdf-extractor):
82edf2d (New md files from RunPod)
{
"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>"
}
Para textos pequeños extraídos (el texto típico de una página es menor a 100 KB), la carga en una sola operación es aceptable. Para extracciones de texto muy grandes, se podría utilizar la carga por bloques.
Listado de documentos secundarios (para depuración/administración):
Listado de documentos secundarios (para depuración/administración):
{
"operation": "list-children",
"parent-id": "doc-123"
}
Respuesta:
{
"children": [
{ "id": "doc-123-page-1", "title": "Page 1", "kind": "text/plain" },
{ "id": "doc-123-page-2", "title": "Page 2", "kind": "text/plain" },
...
]
}
Comportamiento visible para el usuario
list-documents comportamiento predeterminado:
SELECT * FROM document WHERE user = ? AND parent_id IS NULL;
Solo los documentos de nivel superior (fuente) aparecen en la lista de documentos del usuario. Los documentos secundarios se filtran de forma predeterminada.
<<<<<<< HEAD Opción de incluir subdocumentos (para administradores/depuración):
Opción de incluir documentos secundarios (para administradores/depuración):
82edf2d (New md files from RunPod)
{
"operation": "list-documents",
"include-children": true
}
Eliminación en cascada
Cuando se elimina un documento padre, todos los documentos hijos deben ser eliminados:
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)
Consideraciones de almacenamiento
Los bloques de texto extraídos duplican el contenido: El PDF original se almacena en Garage. El texto extraído por página también se almacena en Garage.
Este compromiso permite: Interfaz de fragmentación uniforme: El fragmentador siempre recupera por ID. Reanudación/reintento: Se puede reiniciar en la etapa del fragmentador sin volver a extraer el PDF. Depuración: El texto extraído es inspeccionable. Separación de responsabilidades: El extractor de PDF y el fragmentador son servicios independientes.
Para un PDF de 500 MB con 200 páginas que promedian 5 KB de texto por página: Almacenamiento del PDF: 500 MB. Almacenamiento del texto extraído: ~1 MB en total. Sobrecarga: insignificante.
Salida del extractor de PDF
El extractor de PDF, después de procesar un documento:
- Transmite el PDF desde el bibliotecario a un archivo temporal.
- Extrae el texto página por página.
- Para cada página, almacena el texto extraído como un documento secundario a través del bibliotecario.
- Envía los ID de los documentos secundarios a la cola del fragmentador. <<<<<<< HEAD Después de la última línea traducida, incluir una línea final que contenga exactamente: [[END_OF_TRANSLATION]] =======
82edf2d (New md files from RunPod)
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)
El componente de segmentación recibe estos ID de elementos secundarios y los procesa de la misma manera que procesaría un documento de texto subido por el usuario.
Actualizaciones del cliente
SDK de Python
El SDK de Python (trustgraph-base/trustgraph/api/library.py) debe manejar
las cargas fragmentadas de forma transparente. La interfaz pública permanece sin cambios:
# 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"]
)
Internamente, el SDK detecta el tamaño del documento y cambia de estrategia:
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)
Callbacks de progreso (mejora opcional):
def add_document(self, ..., on_progress=None):
"""
on_progress: Optional callback(bytes_sent, total_bytes)
"""
Esto permite que las interfaces de usuario muestren el progreso de la carga sin cambiar la API básica.
Herramientas de línea de comandos
tg-add-library-document continúa funcionando sin cambios:
# Works transparently for any size - SDK handles chunking internally
tg-add-library-document --file large-report.pdf --title "Large Report"
Se podría agregar una visualización opcional del progreso:
tg-add-library-document --file large-report.pdf --title "Large Report" --progress
# Output:
# Uploading: 45% (225MB / 500MB)
Herramientas heredadas eliminadas:
tg-load-pdf - obsoleto, usar tg-add-library-document
tg-load-text - obsoleto, usar tg-add-library-document
Comandos de administración/depuración (opcional, baja prioridad):
# 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
Estas podrían ser banderas en el comando existente en lugar de herramientas separadas.
Actualizaciones de la Especificación de la API
La especificación OpenAPI (specs/api/paths/librarian.yaml) necesita actualizaciones para:
Nuevas operaciones:
begin-upload - Inicializar sesión de carga por partes
upload-chunk - Cargar parte individual
complete-upload - Finalizar carga
abort-upload - Cancelar carga
get-upload-status - Consultar el progreso de la carga
list-uploads - Listar cargas incompletas para el usuario
stream-document - Recuperación de documentos en streaming
add-child-document - Almacenar texto extraído (interno)
list-children - Listar documentos secundarios (administrador)
Operaciones modificadas:
list-documents - Agregar parámetro include-children
Nuevos esquemas:
ChunkedUploadBeginRequest
ChunkedUploadBeginResponse
ChunkedUploadChunkRequest
ChunkedUploadChunkResponse
UploadSession
UploadProgress
Actualizaciones de la especificación WebSocket (specs/websocket/):
Reflejar las operaciones REST para clientes WebSocket, lo que permite actualizaciones de progreso en tiempo real durante la carga.
Consideraciones de la Experiencia de Usuario
Las actualizaciones de la especificación de la API permiten mejoras en la interfaz de usuario:
Interfaz de usuario del progreso de la carga: Barra de progreso que muestra las partes cargadas Tiempo estimado restante Capacidad de pausa/reanudación
Recuperación de errores: Opción de "reintentar la carga" para cargas interrumpidas Lista de cargas pendientes al reconectar
Manejo de archivos grandes: Detección del tamaño del archivo en el lado del cliente Carga automática por partes para archivos grandes <<<<<<< HEAD Retroalimentación clara durante cargas largas
Retroalimentación clara durante las cargas largas
82edf2d (New md files from RunPod)
Estas mejoras en la experiencia de usuario requieren trabajo en la interfaz de usuario, guiado por la especificación de la API actualizada.