---
{
  "title": "$batch-validate: Valide todos los recursos almacenados según sus perfiles, a escala",
  "description": "Aidbox 2607 reemplaza la antigua API de validación por lotes con la operación $batch-validate — valide un tipo de recurso completo ya existente en su base de datos, de forma síncrona o asíncrona, y analice exactamente qué recursos no son conformes y por qué.",
  "date": "2026-07-13",
  "author": "Andrew Listopadov",
  "reading-time": "9 min read",
  "tags": ["Aidbox", "FHIR Profiling", "Compliance", "Database"],
  "utm-campaign": "feature",
  "utm-content": "batch-validate",
  "tldr": "$batch-validate comprueba cada recurso de un tipo ya almacenado en Aidbox frente a su esquema FHIR y los perfiles que usted indique, de forma síncrona o asíncrona. Los resultados se agregan en un formato compacto e indexado por incidencia, de modo que validar 100 GB de datos no conformes no añade 100 GB a su base de datos. Disponible desde Aidbox 2607.",
  "seo-tags": ["FHIR validation", "batch validation", "FHIR profile validation", "Aidbox", "FHIR conformance", "healthcare data quality"]
}
---

> For the complete documentation index, see [llms.txt](https://www.health-samurai.io/llms.txt).
> Use it to discover all available pages before guessing URLs.

---

Un servidor FHIR acumula registros médicos complejos y profundamente estructurados provenientes de todos los sistemas que lo alimentan, y es fácil asumir que todos son correctos.
Confiar en los datos es una cosa; poder verificarlos — de forma robusta y a escala — es otra.
Con millones de recursos, realizar la validación manual de cada uno resulta tedioso y probablemente poco razonable.
Además, rara vez se valida una sola vez, porque los perfiles no son estáticos.
Guías de implementación como US Core y las guías HL7 Da Vinci publican nuevas versiones con regularidad, y cada una añade elementos, ajusta cardinalidades o modifica los conjuntos de valores a los que se vinculan.
Cada vez que adopta una nueva versión de perfil — o publica una propia — surge la misma pregunta: cuánto de lo que ya almacena sigue siendo conforme.
Entonces, ¿cómo verificar todo ello, de forma repetida, sin que se convierta en un proyecto en sí mismo?

La operación `$validate` de FHIR podría automatizarse en teoría, pero tendría que recuperar cada recurso y volver a enviarlo mediante POST, recopilando y filtrando los resultados.
Este enfoque presenta numerosos problemas y es difícil de escalar.
Lo ideal sería poder pedirle al servidor que valide un tipo de recurso específico y le informe de los errores en una sola llamada, con una capacidad de escalado tanto horizontal como vertical.

Eso es precisamente lo que hace `$batch-validate`.
Se incluye en Aidbox 2607 y reemplaza por completo la anterior API de validación por lotes.

## Por qué reconstruimos la validación por lotes

Aidbox ha contado con validación por lotes asíncrona durante años, expuesta mediante un conjunto de RPCs (`aidbox.validation/batch-validation` y similares).
Funcionaba, pero tenía varios problemas.
Por un lado, **cada error de validación se almacenaba como su propio recurso `BatchValidationError`.**
Además, era bastante lento, lo que convertía la validación de grandes volúmenes de recursos en una tarea innecesariamente prolongada.

Por último, validar un conjunto de datos extenso podía generar tantos resultados que podían rivalizar con los propios datos en tamaño.
Cien gigabytes de recursos no conformes podían producir algo cercano a cien gigabytes de recursos de error.
El mecanismo al que se recurría para *entender* un problema de calidad de datos empeoraba el problema de almacenamiento.

Además, era exclusivamente asíncrono, tenía forma de RPC en lugar de operación FHIR, y le entregaba un conjunto de recursos de error para consultar en vez de una respuesta directa.

`$batch-validate` conserva la parte útil — validar lo que ya está almacenado, en paralelo — y corrige el resto.

|                | Validación por lotes antigua                  | `$batch-validate`                                                     |
|----------------|-----------------------------------------------|-----------------------------------------------------------------------|
| Interfaz       | RPCs propietarios                             | Operación FHIR (`Parameters` de entrada y salida)                     |
| Modos          | Solo asíncrono                                | Síncrono **o** asíncrono                                              |
| Almacenamiento de resultados | Un recurso `BatchValidationError` por error | Una fila por incidencia **distinta** más una tabla compacta de IDs de recursos |
| Coste de almacenamiento | Crece con el número de errores         | Acotado — los cuerpos de los recursos nunca se copian                 |
| Salida         | Un conjunto de recursos de error para consultar | Resumen de incidencias ordenado por gravedad con desglose bajo demanda |
| Escalado       | Fijo                                          | Fragmentos con particionado por hash, transmitidos en flujo, paralelos entre nodos e indexables |

Los RPCs `aidbox.validation/*` antiguos y los recursos `BatchValidationRun` / `BatchValidationError` ya no existen. Esto es un cambio disruptivo; si los utilizaba, migre a la operación que se describe a continuación.

## Cómo usar

`$batch-validate` se ejecuta sobre un único tipo de recurso.
El único parámetro obligatorio es `_since`, un límite inferior sobre `meta.lastUpdated`.
Esto obliga a que cada ejecución declare una ventana temporal en lugar de analizar todo el conjunto de datos por accidente — para validar todo, pase la época Unix.

Así, para validar todas las Observations actualizadas en abril de 2026, podemos invocar `$batch-validate` de la siguiente manera:

```yaml
POST /fhir/Observation/$batch-validate
Content-Type: application/json

resourceType: Parameters
parameter:
  - {name: _since, valueInstant: "2026-04-01T00:00:00Z"}
  - {name: _until, valueInstant: "2026-05-01T00:00:00Z"}
```

Por defecto, la llamada es **síncrona**: bloquea y devuelve un resumen `Parameters` con los recuentos principales y una entrada por incidencia distinta, ordenadas de mayor a menor gravedad.

```yaml
resourceType: Parameters
parameter:
  - {name: task-id,   valueString: "b1f9..."}
  - {name: validated, valueUnsignedInt: 1804646}   # recursos comprobados
  - {name: valid,     valueUnsignedInt: 1317494}   # sin incidencias
  - {name: invalid,   valueUnsignedInt: 487152}    # total de recursos no válidos
  - {name: invalid-resources, valueUrl: "/fhir/$batch-validate/b1f9.../invalid-resources"}
  - name: issue
    part:
      - {name: code,        valueCode: invalid-slice-cardinality}
      - {name: expression,  valueString: category}
      - {name: profile,     valueString: "http://hl7.org/fhir/us/core/StructureDefinition/us-core-observation-lab"}
      - {name: count,       valueUnsignedInt: 486018}   # recursos que presentan exactamente esta incidencia
      - {name: diagnostics, valueString: "Observation.category: element count is outside the allowed range"}
```

`count` es el número de recursos distintos que presentan exactamente esa incidencia — la forma más rápida de comprobar si un problema afecta a seis recursos o a seiscientos mil.

Las llamadas síncronas son adecuadas para un conjunto pequeño de recursos cuando se desea obtener la respuesta de inmediato.
El trabajo se ejecuta igualmente en paralelo: `number-of-chunks` (configurado por llamada) divide los recursos en ese número de fragmentos, y el parámetro [`scheduler-executors`](https://www.health-samurai.io/docs/aidbox/reference/all-settings#scheduler-executors) controla cuántos se ejecutan simultáneamente en el nodo.
Juntos permiten equilibrar la granularidad de los fragmentos con la carga de trabajo de una sola máquina — ese es el control de escalado vertical.

Sin embargo, cuando se desea validar un conjunto de datos considerablemente mayor, puede resultar conveniente hacerlo de forma asíncrona.

## Asíncrono para grandes conjuntos de datos

Para que cualquier llamada a `$batch-validate` sea asíncrona, basta con añadir la cabecera `Prefer: respond-async`.
Aidbox programa entonces el trabajo en su motor de tareas, distribuyéndolo entre nodos y permitiendo tanto el escalado horizontal como el vertical.

```yaml
POST /fhir/Observation/$batch-validate
Prefer: respond-async

resourceType: Parameters
parameter:
  - {name: _since, valueInstant: "1970-01-01T00:00:00Z"} # básicamente validará cada Observation en la base de datos
```

Recibirá un `202` con una cabecera `Content-Location` que puede consultar para ver el progreso:

```http
GET /fhir/$batch-validate/b1f9...
```

Mientras se ejecuta, obtendrá un `202` con una cabecera `X-Progress: 45%`.
Cuando finalice, recibirá el mismo resumen `Parameters` que devuelve una llamada síncrona.
Tanto las llamadas síncronas como las asíncronas persisten sus resultados bajo un `task-id`, por lo que no hay diferencia en la forma de analizar los resultados.

## Explorar los recursos no válidos

El resumen le indica qué incidencias existen y cuántos recursos presenta cada una.
Para ver los recursos reales, siga el enlace `invalid-resources` — filtre a una sola incidencia con `_issue` y navegue con `_count` / `_page`:

```http
GET /fhir/$batch-validate/b1f9.../invalid-resources?_count=50&_page=1
```

Cada recurso no válido se devuelve con una `fullUrl` **específica de la versión** que apunta a la versión exacta que fue validada, el cuerpo del recurso y un `OperationOutcome` que enumera todas las incidencias de ese recurso:

```yaml
- name: resource
  part:
    - {name: fullUrl, valueUrl: "/Observation/obs-42/_history/7"}
    - name: resource
      resource: {resourceType: Observation}
    - name: outcome
      resource:
        resourceType: OperationOutcome
        issue:
          - {severity: fatal, code: invalid, expression: [Observation.category], diagnostics: "..."}
```

El resultado lista el conjunto **completo** de incidencias de un recurso, incluso cuando `_issue` limita qué recursos se devuelven — de modo que nunca corregirá un problema para descubrir un segundo en el siguiente pase.

## Probar un perfil antes de aplicarlo

La razón más habitual para ejecutar esta operación es un nuevo perfil: desea saber qué falla antes de convertirlo en un requisito.
Pase uno o más canonicals de `profile` y cada recurso se validará frente a ellos además de su esquema base.

```yaml
resourceType: Parameters
parameter:
  - {name: _since,  valueInstant: "1970-01-01T00:00:00Z"}
  - {name: profile, valueCanonical: "http://hl7.org/fhir/us/core/StructureDefinition/us-core-observation-lab"}
```

Varios perfiles son conjuntivos (AND): un recurso es conforme solo si cumple con todos ellos, y las incidencias son la unión de todos.
Así puede activar US Core sabiendo exactamente qué fallaría, en lugar de descubrirlo en producción.

## Ajustar el validador por ejecución

A veces se desea un análisis estructural rápido sin importar la terminología; otras veces se quiere ser más estricto que los valores predeterminados del sistema.
Cada parámetro `disable-*` / `strict-*` sobreescribe un ajuste del validador únicamente para esa ejecución — si se omite, se mantiene el comportamiento configurado en el sistema.

| Parámetro                        | Efecto                                                                        |
|----------------------------------|-------------------------------------------------------------------------------|
| `disable-terminology-validation` | Omitir comprobaciones de vinculación de códigos y terminología                |
| `disable-primitive-validation`   | Omitir comprobaciones de tipo primitivo y formato                             |
| `disable-slicing-validation`     | Omitir la validación de slices                                                |
| `disable-constraint-validation`  | Omitir **todos** los invariantes FHIRPath (o comprobarlos todos si es `false`) |
| `disable-constraint`             | Omitir invariantes específicos por clave (p. ej. `us-core-8`)                 |
| `strict-profile-resolution`      | Tratar un perfil no resuelto como error en lugar de omitirlo                  |
| `strict-extension-resolution`    | Tratar una extensión no resuelta como error                                   |

Vale la pena destacar `strict-profile-resolution`.
Sin él, una URL de perfil que no se resuelve se omite, por lo que un error tipográfico se convierte en un informe de «conformidad» que es silenciosamente incorrecto.
Actívelo cuando desee que la ejecución falle de forma explícita.

## Diseñado para escalar

Internamente, `$batch-validate` particiona los recursos por hash en un número fijo de fragmentos (`number-of-chunks`, por defecto 12).
Cada fragmento valida su porción `mod(hash(id), N)`, y los fragmentos se agregan en un único resultado:

```mermaid
flowchart LR
    A["POST /fhir/Observation/$batch-validate"] --> B{Hash-partition by id}
    B --> C[chunk 0]
    B --> D[chunk 1]
    B --> E[chunk ...]
    C --> F[(Aggregated results)]
    D --> F
    E --> F
```

Cada fragmento se transmite en flujo, por lo que el uso de memoria permanece acotado independientemente del tamaño del conjunto de datos.
Una ejecución síncrona ejecuta sus fragmentos en un grupo de trabajadores dedicado — un grupo de hilos fijo que Aidbox crea para esa ejecución y destruye al finalizar, dimensionado por el parámetro [`scheduler-executors`](https://www.health-samurai.io/docs/aidbox/reference/all-settings#scheduler-executors) y separado de los hilos que atienden el tráfico normal de la API.
Como máximo, ese número de fragmentos se ejecuta a la vez, por lo que no hay límite superior para `number-of-chunks` — un número mayor simplemente se encola en lugar de aumentar el uso de memoria.
Una ejecución asíncrona no utiliza este grupo local; en su lugar, se ejecuta en el grupo propio del motor de tareas, escribiendo una fila de programador por fragmento para que cualquier nodo la reclame.
Como no toma prestados hilos de solicitud ni ranuras del grupo de conexiones, es segura en un sistema en producción; la CPU es el recurso que se consume, por lo que se recomienda el modo asíncrono para una primera pasada de grandes volúmenes.

La ruta asíncrona es también la que permite que una ejecución escale entre máquinas.
Cada fragmento es un trabajo en un programador que reside en el Postgres compartido, por lo que puede ejecutar varias instancias de Aidbox en diferentes máquinas contra una misma base de datos y estas se repartirán los fragmentos entre sí — la ejecución se acelera a medida que se añaden nodos.
Un fragmento es reclamado por exactamente una instancia, por lo que nunca se ejecuta dos veces; y si una instancia falla a mitad de una ejecución, el programador reclama su fragmento y lo reintenta en otra, con escrituras idempotentes para que un fragmento reclamado nunca duplique el conteo.

```mermaid
flowchart LR
    A["POST + Prefer: respond-async"] --> Q[(Shared Postgres chunk queue)]
    Q -->|claim| N1[Aidbox node 1]
    Q -->|claim| N2[Aidbox node 2]
    Q -->|claim| N3[Aidbox node ...]
    N1 --> R[(Aggregated results)]
    N2 --> R
    N3 --> R
```

Dado que `N` es fijo para una ejecución, el predicado de partición es una expresión constante que se puede indexar.
Para un conjunto de datos muy grande validado con un número elevado de fragmentos, un índice de expresión coincidente convierte cada fragmento de un escaneo completo en un escaneo de índice selectivo:

```sql
CREATE INDEX CONCURRENTLY observation_batch_validate_10000
  ON observation (mod(abs(hashtextextended(id, 0)), 10000));
```

Luego ejecute con `number-of-chunks: 10000`.
El módulo del índice debe coincidir con el número de fragmentos, o PostgreSQL no lo utilizará — confirme con `EXPLAIN`.

## Compacto por diseño

He aquí por qué validar 100 GB de datos erróneos ya no le cuesta otros 100 GB.
Aidbox almacena los resultados en un formato agregado e indexado por incidencia en un esquema dedicado `aidbox_batch_validation`:

| Tabla              | Contiene                                                                    |
|--------------------|-----------------------------------------------------------------------------|
| `issue`            | una fila por error **distinto**                                             |
| `invalid_resource` | una fila compacta `(issue, resource_id, version)` por recurso — solo IDs   |
| `chunk_stat`       | una fila por fragmento con sus métricas                                     |

Todas las ocurrencias que comparten perfil, tipo de recurso, ruta normalizada, código y restricción se consolidan en una única incidencia cuyo recuento es el número de recursos distintos que la presentan.
Aidbox nunca copia los cuerpos de los recursos no válidos ni sus `OperationOutcome`s: el desglose vuelve a leer cada cuerpo desde el historial en la versión validada y reconstruye el resultado a partir de los campos almacenados.
Son los problemas distintos, no el volumen bruto, los que determinan el coste de almacenamiento.

## Pruébelo

`$batch-validate` está disponible en Aidbox 2607.
Apúntelo a un tipo de recurso, pase un `_since` de época Unix y obtendrá en una sola llamada un mapa de los problemas de calidad de sus datos ordenado por gravedad — para luego desglosar los recursos exactos detrás de cada uno.

¿Desea verlo funcionar sin escribir nada?
Hemos publicado un notebook interactivo que ejecuta el flujo completo de principio a fin: carga un conjunto de Patients de muestra, crea un perfil basado en US Core Patient, los valida en una sola llamada y representa gráficamente los resultados — la distribución de válidos e inválidos, los Patients no válidos por incidencia y dónde se concentran los problemas.
Abra el notebook **Batch validation** desde la sección Notebooks en Aidbox y ejecútelo de arriba a abajo.

Referencia completa: [Validación de recursos por lotes](https://www.health-samurai.io/docs/aidbox/modules/profiling-and-validation/batch-resource-validation).