Files
IPSExtractor/services/services-count-facturas-medicas-serving
..

Services Count — API de Facturas Médicas

Go

Microservicio de inferencia en Go que cuenta servicios facturados en facturas médicas PDF. El entrenamiento del modelo vive en el repositorio hermano services-count-facturas-medicas-ml; este repositorio despliega la API HTTP y los workers de procesamiento.

Características

  • Conteo síncrono (1–50 PDFs por petición) y asíncrono (hasta 50 000 PDFs por job).
  • Pipeline de inferencia: OCR → clustering DBSCAN → Random Forest vía ONNX.
  • Escalado horizontal de workers con Docker Compose.
  • Especificación OpenAPI y descriptor de herramientas para agentes AI.

Arquitectura

El sistema se compone de una API HTTP, workers consumidores y tres servicios de infraestructura (Redis, NATS JetStream y MinIO), definidos en docker-compose.yml.

flowchart TB
    Client[Cliente_o_Agente]
    API[API_Go_8080]
    Worker[Worker_Go_xN]
    Redis[(Redis)]
    NATS[NATS_JetStream]
    MinIO[(MinIO)]
    Pipeline[OCR_Tesseract_ONNX]

    Client -->|sync_POST_v1_count| API
    Client -->|async_POST_v1_jobs| API
    API --> Pipeline
    API --> Redis
    API --> MinIO
    API -->|publish| NATS
    NATS -->|consume| Worker
    Worker --> MinIO
    Worker --> Pipeline
    Worker --> Redis

Pipeline de inferencia

Por cada PDF se procesa la primera página siguiendo este flujo (internal/pipeline):

flowchart LR
    PDF[PDF_primera_pagina] --> Render[Render_MuPDF]
    Render --> OCR[Tesseract_spa]
    OCR --> Cluster[DBSCAN_filas]
    Cluster --> Features[8_features]
    Features --> Infer[ONNX_RandomForest]
    Infer --> Result[service_count_y_services]
  1. Render — La primera página se rasteriza con MuPDF (go-fitz) a 300 DPI.
  2. OCR — Tesseract extrae cajas de texto en español.
  3. Clustering — DBSCAN agrupa palabras en filas candidatas.
  4. Features — Se calculan 8 atributos por fila (códigos, moneda, posición, etc.).
  5. Inferencia — Un Random Forest exportado a ONNX clasifica cada fila como servicio o no.

Componentes

Componente Binario / servicio Paquete Responsabilidad
API HTTP cmd/api internal/api Endpoints REST, gestión de jobs, health probes
Worker cmd/worker internal/worker Consume NATS, descarga PDFs de MinIO, persiste resultados
Pipeline — internal/pipeline Orquestación OCR → clustering → features → inferencia
Modelo ONNX — internal/model Carga model.onnx e inference.json
OCR — internal/ocr Render PDF (go-fitz) + Tesseract (gosseract)
Estado de jobs Redis 7 internal/store Metadatos, resultados y progreso (TTL 72 h)
Cola NATS JetStream internal/queue Stream PDF_JOBS, subject pdf.process.*
Object storage MinIO internal/storage Almacenamiento de PDFs en jobs asíncronos

Estructura del repositorio

├── cmd/
│   ├── api/              # Punto de entrada HTTP
│   └── worker/           # Punto de entrada worker
├── internal/
│   ├── api/              # Servidor HTTP (chi)
│   ├── config/           # Variables de entorno
│   ├── domain/           # Tipos de dominio
│   ├── model/            # Inferencia ONNX
│   ├── ocr/              # Extracción OCR
│   ├── pipeline/         # Procesamiento por PDF
│   ├── queue/            # NATS JetStream
│   ├── storage/          # Cliente MinIO
│   ├── store/            # Estado en Redis
│   └── worker/           # Runner del worker
├── api/openapi.yaml      # Especificación OpenAPI 3.1
├── mcp/tool.json         # Descriptor de herramientas para agentes
├── web/                  # Cliente demo (embebido en la API en /demo/)
├── scripts/              # Export ONNX, paridad con Python
└── models/latest/        # model.onnx + inference.json (montado en Docker)

Modos de operación

Modo Endpoint principal Límite Cuándo usarlo
Síncrono POST /v1/count 50 PDFs Pruebas, integraciones en tiempo real, lotes pequeños
Asíncrono POST /v1/jobs + poll 50 000 PDFs/job MinIO + NATS + workers (path producción)

Flujo asíncrono (producción):

  1. Crear job (POST /v1/jobs) — shell vacío, prefijo MinIO o lista de claves.
  2. Subir PDFs por chunks (POST /v1/jobs/{jobID}/upload, máx. 500 por chunk) → MinIO Put → NATS Publish.
  3. Workers consumen la cola, descargan de MinIO, OCR, guardan en Redis.
  4. Consultar progreso (GET /v1/jobs/{jobID}?wait_ms=30000 para long-polling).
  5. Obtener resultados paginados (GET /v1/jobs/{jobID}/results?cursor=&limit=).

API REST

Especificación completa: GET /openapi.json o api/openapi.yaml.

Método Ruta Descripción
GET /health Liveness probe
GET /ready Readiness probe
POST /v1/count Conteo síncrono (multipart files)
POST /v1/jobs Crear job asíncrono
POST /v1/jobs/{jobID}/upload Subir chunk de PDFs
GET /v1/jobs/{jobID} Estado y progreso del job
GET /v1/jobs/{jobID}/results Resultados paginados
DELETE /v1/jobs/{jobID} Cancelar job

Parámetros relevantes:

  • include_rejected (query) — Si es true, incluye todas las filas OCR con el campo is_service.
  • Jobs asíncronos devuelven 202 Accepted; la cola rechaza con 429 si supera QUEUE_MAX_PENDING.

Respuesta por PDF:

{
  "filename": "factura.pdf",
  "service_count": 2,
  "status": "ok",
  "services": [
    { "text": "890201 Consulta ...", "probability": 0.94 }
  ]
}

Stack tecnológico

Área Tecnología
Lenguaje Go 1.23
HTTP router go-chi/chi v5
OCR go-fitz (MuPDF) + gosseract (Tesseract)
ML ONNX Runtime + Random Forest exportado
Cola NATS JetStream
Estado Redis 7
Storage MinIO
Contenedores Docker multi-stage (Dockerfile)

Inicio rápido

1. Exportar el modelo ONNX

Desde el repositorio de entrenamiento, generar los artefactos de inferencia:

pip install -r requirements-export.txt
python scripts/export_onnx.py \
  --joblib ../services-count/models/latest/model.joblib \
  --metadata ../services-count/models/v1.0.0/model_metadata.json

Esto produce models/latest/model.onnx e inference.json.

2. Levantar con Docker Compose

docker compose up --build
Servicio Puerto / acceso
API http://localhost:8080
MinIO Console http://localhost:9002
Workers 1 réplica inicial; autoscaler sube hasta 8 según cola
Autoscaler Escala worker vía Docker socket según PendingCount en Redis

3. Probar

# Síncrono
curl -F "files=@factura.pdf" http://localhost:8080/v1/count

# Asíncrono (PDFs ya en MinIO bajo un prefijo)
curl -X POST http://localhost:8080/v1/jobs \
  -H "Content-Type: application/json" \
  -d '{"prefix":"jobs/batch-1/"}'

Cliente web de ejemplo

Con la API en ejecución, abre http://localhost:8080/demo/ para probar la integración desde el navegador. Los archivos estáticos se sirven desde la propia API (mismo origen, sin CORS).

Pestaña Demuestra
Síncrono POST /v1/count con subida multipart de hasta 50 PDFs
Asíncrono POST /v1/jobs → upload → polling de estado → resultados paginados

El modo asíncrono requiere que workers, Redis, NATS y MinIO estén levantados (igual que con docker compose up). Código fuente en web/.

Desarrollo local

Requisitos adicionales a Go 1.23+:

  • Tesseract con datos de idioma spa
  • ONNX Runtime (la variable ONNXRUNTIME_SHARED_LIBRARY apunta a la librería compartida)
  • Redis, NATS y MinIO en ejecución
go build -o api ./cmd/api
go build -o worker ./cmd/worker

Configurar las variables de entorno para apuntar a los servicios locales (ver tabla siguiente).

Configuración

Variables definidas en internal/config/config.go:

Variable Default Descripción
PORT 8080 Puerto de la API
REDIS_URL redis://redis:6379/0 Conexión Redis
NATS_URL nats://nats:4222 Conexión NATS
NATS_STREAM PDF_JOBS Nombre del stream JetStream
NATS_SUBJECT_PREFIX pdf.process Prefijo de subjects
MINIO_ENDPOINT minio:9000 Endpoint MinIO
MINIO_ACCESS_KEY minioadmin Access key
MINIO_SECRET_KEY minioadmin Secret key
MINIO_BUCKET pdf-count Bucket de PDFs
MINIO_USE_SSL false Usar SSL con MinIO
MODEL_PATH /app/models/model.onnx Ruta al modelo ONNX
INFERENCE_CONFIG /app/models/inference.json Metadata de inferencia
OCR_DPI 300 DPI de renderizado PDF (150 reduce latencia ~2–4×)
OCR_LANG spa Idioma Tesseract
DBSCAN_EPS 10 Parámetro epsilon de DBSCAN
DBSCAN_MIN_SAMPLES 1 Mínimo de muestras DBSCAN
SYNC_MAX_FILES 50 Máximo de PDFs en modo síncrono
JOB_MAX_FILES 50000 Máximo de PDFs por job
QUEUE_MAX_PENDING 100000 Límite de mensajes pendientes en cola
WORKER_CONCURRENCY 4 Goroutines paralelas por worker
MAX_PDF_SIZE_MB 20 Tamaño máximo por PDF
PDF_PROCESS_TIMEOUT_SEC 120 Timeout de procesamiento por PDF
JOB_RESULT_TTL_HOURS 72 TTL de resultados en Redis
OCR_POOL_SIZE 4 (API) / WORKER_CONCURRENCY (worker) Clientes Tesseract reutilizables en pool
SYNC_PROCESS_CONCURRENCY 4 (o OCR_POOL_SIZE) PDFs procesados en paralelo en /v1/count
JOB_INLINE_PROCESSING false true procesa uploads en la API sin MinIO (solo dev local)
LOG_PIPELINE_TIMING false Logs de duración por etapa del pipeline
NATS_FETCH_MAX_WAIT_MS 300 Tiempo máximo de espera del worker al leer NATS

Rendimiento

El cuello de botella de /v1/count es el OCR con Tesseract, no la inferencia ONNX. Los clientes Tesseract se crean una vez al arranque y se reutilizan desde un pool (internal/ocr/pool.go).

Etapa Tiempo típico (1 PDF)
Render PDF (MuPDF) ~100–300 ms
OCR Tesseract ~3–8 s
Clustering + ONNX ~10–50 ms

Latencia esperada (síncrono): ~3–10 s por PDF. Varios PDFs se procesan en paralelo (SYNC_PROCESS_CONCURRENCY, default 4). Ejemplo: 20 PDFs ≈ ceil(20/4) × 2.85s ≈ 15s.

Latencia esperada (asíncrono por upload): path producción MinIO → NATS → worker. Con WORKER_CONCURRENCY=4 y autoscaler activo, 16 PDFs ≈ ceil(16/4) × 2.85s más overhead Put/Get (~15–25 s). 100 PDFs escala hasta 8 workers (32 slots OCR).

Asíncrono por prefix MinIO (POST /v1/jobs {"prefix":"..."}): mismos workers; los PDFs ya están en el bucket.

Ajustes para reducir latencia:

  • OCR_DPI=150 — reduce tiempo de render y OCR (~2–4× más rápido) con posible ligera pérdida de precisión. Validar con scripts/compare_parity.py antes de usar en producción.
  • OCR_POOL_SIZE=4 y WORKER_CONCURRENCY=4 — paralelismo por réplica worker.
  • Upload pipeline publica en NATS tras cada Put (no al final del lote).
  • LOG_PIPELINE_TIMING=true — logs por etapa: ocr_render, ocr_tesseract, cluster, infer, total.
  • NATS_FETCH_MAX_WAIT_MS=300 — el worker recoge mensajes con menor espera.
  • GET /v1/jobs/{id}?wait_ms=30000 — long-polling en lugar de poll cada 500 ms.

Escalado

Autoscaler (docker compose)

El servicio autoscaler lee PendingCount en Redis cada 5 s y ajusta réplicas worker:

desired = clamp(ceil(pending / WORKER_SLOTS), MIN_WORKERS, MAX_WORKERS)
Variable Default Descripción
MIN_WORKERS 1 Réplicas mínimas
MAX_WORKERS 8 Réplicas máximas
WORKER_SLOTS 4 PDFs en paralelo por réplica (WORKER_CONCURRENCY)
SCALE_DOWN_IDLE_SEC 60 Segundos sin cola antes de bajar 1 réplica

Requiere montar /var/run/docker.sock (solo entorno local). En Kubernetes usar HPA sobre lag de NATS o métricas custom.

Escalado manual

docker compose up --scale worker=8 -d

Cada worker procesa hasta WORKER_CONCURRENCY PDFs en paralelo. NATS JetStream distribuye los mensajes entre réplicas con política work-queue.

Integración con agentes (MCP)

mcp/tool.json es un descriptor declarativo de herramientas para agentes AI (Cursor, etc.). No incluye un servidor MCP embebido.

Herramienta Modo Flujo
count_pdf_services Síncrono POST /v1/count con multipart
count_pdf_services_batch Asíncrono Crear job → poll estado → resultados paginados

Paridad con Python

Scripts para validar que la inferencia Go reproduce los resultados del pipeline Python original:

set SERVICES_COUNT_ROOT=../services-count
python scripts/parity_baseline.py
python scripts/compare_parity.py

Troubleshooting

Síntoma Causa probable Acción
API no arranca Modelo ONNX ausente Ejecutar scripts/export_onnx.py y verificar models/latest/
/ready falla Redis o NATS no disponibles Comprobar healthchecks en docker compose ps
Jobs en pending sin avance Workers no consumen cola Verificar logs del worker y conectividad a NATS/MinIO
Job atascado en running con N/N procesados Race en metadata del job (versiones antiguas) Reconstruir con docker compose up --build; GetJob ahora finaliza jobs completados
429 al crear job Cola saturada Escalar workers o esperar a que baje QUEUE_MAX_PENDING
Resultados vacíos o erróneos OCR o modelo desactualizado Revisar OCR_LANG=spa, reexportar ONNX desde el repo ML