# Analisis matematico y computacional del modelo de consulta INVIMA ## 1. Resumen tecnico del modelo real implementado El sistema vigente implementa una plataforma de consulta sobre datos abiertos del INVIMA publicados en Socrata/datos.gov.co. La fuente se configura en `backend/APIinvima.json`, con base URL `https://www.datos.gov.co/resource` y ocho datasets: `dispositivos`, `rs_nso`, `medicamentos`, `cum_vigentes`, `homeopaticos`, `fitoterapeuticos`, `suplementos` y `vacunas`. El flujo real es: 1. Extraccion: `fetchInvimaDataset` construye URLs SODA con `$limit`, `$offset`, `$select`, `$where` y `$order`, aplica token opcional, timeout, reintentos y backoff. 2. Normalizacion por dataset: `invimaAdapters` convierte columnas heterogeneas de cada dataset hacia un esquema comun. En este paso se limpian textos, se parsean fechas y se genera `source_uid` con SHA-1. 3. Persistencia: `persistBatch` guarda cada lote en dos estructuras: `invima_api_raw`, con el JSON crudo, e `invima_api_catalog`, con campos normalizados. 4. Clasificacion y enriquecimiento: `normalizeCatalogRecord` calcula `categoria_canonica`, `categoria_slug` y `search_text`. 5. Indexacion: `ensureInvimaTables` crea indices por dataset, categoria, estado, registro, expediente, modalidad, grupo, fechas, FTS en espanol y trigramas opcionales. 6. Consulta: `getInvimaCatalog` parsea filtros, ejecuta `runCatalogQuery` y usa `to_tsvector/websearch_to_tsquery` o `ILIKE` como respaldo. 7. Presentacion: Angular consulta `/api/invima/catalogo`, muestra tabla paginada y permite exportar CSV/XLSX por `/api/invima/catalogo/export`. Los archivos historicos `Productos.csv`, `products_comands.py`, `databasepg.js`, `comandos.txt` y `comandos en pgadmin4.txt` pertenecen al flujo legacy de tabla `productos`. El `README.md` del backend indica que ese flujo fue eliminado del modelo vigente y reemplazado por el catalogo unico `invima_api_catalog`. ## 2. Variables principales del modelo En el codigo, la variable independiente primaria no es solo "dispositivo medico"; es el conjunto de criterios de consulta enviado por el usuario: \[ c = (q, categorias, datasets, estados, modalidades, grupos, fechaVencDesde, fechaVencHasta, fechaExpDesde, fechaExpHasta, onlyVigentes, limit, offset) \] Cuando `q` representa un producto o dispositivo medico, el producto puede tratarse como variable independiente conceptual: \[ x = producto \] Sin embargo, el buscador real aplica `q` sobre `search_text`, `registro_sanitario`, `expediente`, `producto`, `categoria_canonica` y `categoria_linea`. Por tanto, `q` es una variable independiente textual general. Variables dependientes implementadas en `invima_api_catalog`: - Identificacion/fuente: `source_dataset_key`, `source_dataset_id`, `source_uid`, `source_dataset_name`. - Clasificacion: `categoria_linea`, `categoria_canonica`, `categoria_slug`, `grupo`, `modalidad`. - Registro/producto: `expediente`, `registro_sanitario`, `producto`, `marca`. - Actor regulatorio/comercial: `titular`, `rol_nombre`, `rol_tipo`, `ciudad_titular`, `pais_titular`, `fabricante`, `importador`. - Estado/tiempo: `estado_registro`, `vigencia`, `fecha_expedicion`, `fecha_vencimiento`. - Campos especificos de medicamentos/tecnologias: `principio_activo`, `forma_farmaceutica`, `presentacion_comercial`, `atc`. - Campos tecnicos: `search_text`, `extra`, `fetched_at`, `updated_at`. Campos fundamentales para identificar un registro: `source_dataset_key + source_uid` como clave primaria, y como identificadores semanticos `registro_sanitario`, `expediente`, `producto` y `source_dataset_key`. Los campos complementarios son los atributos regulatorios, comerciales, de clasificacion y de vigencia. ## 3. Dominio formal de las variables Sea `R` la relacion `invima_api_catalog`. Los dominios implementados pueden definirse asi: \[ D_{dataset} = \{source\_dataset\_key\} \] \[ D_{uid} = \{source\_uid\} \] \[ D_{producto} = \{producto \mid producto \in R\} \] \[ D_{registro} = \{registro\_sanitario \mid registro\_sanitario \in R\} \] \[ D_{expediente} = \{expediente \mid expediente \in R\} \] \[ D_{titular} = \{titular \mid titular \in R\} \] \[ D_{estado} = \{estado\_registro \mid estado\_registro \in R\} \] \[ D_{categoria} = \{categoria\_slug, categoria\_canonica, categoria\_linea \mid registros\ de\ R\} \] \[ D_{modalidad} = \{modalidad \mid modalidad \in R\} \] \[ D_{grupo} = \{grupo \mid grupo \in R\} \] \[ D_{fechaExp}, D_{fechaVenc} \subseteq Date \] \[ D_{texto} = vocabulario(search\_text) \] El dominio de busqueda del sistema es el producto cartesiano restringido por registros existentes e indices de PostgreSQL: \[ D_c = D_{texto} \times \mathcal{P}(D_{categoria}) \times \mathcal{P}(D_{dataset}) \times \mathcal{P}(D_{estado}) \times \mathcal{P}(D_{modalidad}) \times \mathcal{P}(D_{grupo}) \times D_{fechaExp}^2 \times D_{fechaVenc}^2 \] ## 4. Relacion entre datos La relacion principal del modelo puede expresarse como: \[ R \subseteq D_{dataset} \times D_{uid} \times D_{categoria} \times D_{expediente} \times D_{registro} \times D_{producto} \times D_{titular} \times D_{estado} \times D_{fechaExp} \times D_{fechaVenc} \times D_{modalidad} \times D_{grupo} \times D_{marca} \times D_{principio} \times D_{forma} \times D_{presentacion} \times D_{atc} \times D_{rol} \times D_{ubicacion} \times D_{fabricante} \times D_{importador} \times D_{vigencia} \times D_{extra} \] Esta formulacion representa correctamente la tabla `invima_api_catalog`, siempre que se entienda que algunos dominios son opcionales o nulos segun el dataset. La relacion cruda complementaria es: \[ R_{raw} \subseteq D_{dataset} \times D_{uid} \times JSONB \times D_{timestamp} \] ## 5. Ecuacion de consulta o filtrado La consulta real combina seleccion, ordenamiento, ranking textual y paginacion: \[ S = \tau_{orden}\left(\sigma_{P_c}(R)\right) \] \[ Q(R,c,limit,offset) = \pi_{limit,offset}(S) \] Donde `P_c(r)` es verdadero si se cumplen las condiciones generadas por `buildCatalogWhere`: texto libre, categorias, datasets, estados, modalidades, grupos, rangos de fechas y vigencia. Si `q` tiene al menos tres caracteres se usa: \[ to\_tsvector(search\_text) \@@ websearch\_to\_tsquery(q) \] con respaldo por `ILIKE` en `registro_sanitario`, `expediente`, `producto`, `categoria_canonica` y `categoria_linea`. Esta formulacion se ajusta mejor que una simple seleccion, porque el codigo tambien calcula `COUNT(*)`, `ts_rank`, `ORDER BY`, `LIMIT` y `OFFSET`. ## 6. Dependencia entre variable independiente y variables dependientes Si el criterio independiente es un producto o dispositivo medico \(x \in D_{producto}\), la salida es multivaluada: \[ g(x) = \{r \in R \mid producto(r) \sim x \lor search\_text(r) \sim x\} \] La proyeccion de atributos dependientes es: \[ Y_x = \pi_{registro\_sanitario, expediente, titular, estado\_registro, categoria\_canonica, modalidad, grupo, fecha\_expedicion, fecha\_vencimiento, vigencia, fabricante, importador, source\_dataset\_name}(g(x)) \] No es una funcion uno a uno: un producto puede aparecer en varios datasets, registros, roles o titulares, y un registro puede generar varias filas si la fuente distingue roles o actores. ## 7. Ecuacion de transformacion de datos El sistema si transforma datos antes de consultarlos. La forma general es: \[ f_d: R_{crudo,d} \rightarrow R_{catalogo} \] Para cada fila cruda \(r_i\) de un dataset \(d\): \[ t_i = I(S(C(M_d(N(L(r_i)))))) \] Donde: - \(L\): limpieza de texto (`cleanText`), eliminando vacios y texto `"null"`. - \(N\): normalizacion de fechas (`parseDateFlexible`) y campos comunes. - \(M_d\): mapeo especifico por dataset (`invimaAdapters`). - \(C\): clasificacion canonica (`normalizeCategory`) y `categoria_slug`. - \(S\): construccion de `search_text`. - \(I\): integracion/upsert transaccional en `invima_api_raw` e `invima_api_catalog`. La ecuacion representa el flujo real, con la salvedad de que no todos los datasets llenan todos los atributos. ## 8. Metricas recomendadas Medibles con el codigo actual: - Numero de filas crudas: `rawRows`. - Numero de filas normalizadas: `catalogRows`. - Filas descargadas, persistidas y paginas por sincronizacion: `fetched`, `persisted`, `pages`. - Datasets exitosos/fallidos durante sincronizacion. - Conteo de categorias, datasets con datos, vigentes y vencidos. - Total de resultados por consulta, `limit` y `offset`. - Duracion aproximada de una sincronizacion por `startedAt` y `finishedAt` del job en memoria. Requieren instrumentacion adicional: - Tiempo promedio/maximo de respuesta por consulta. - Historial persistente de consultas y tiempos. - CPU, memoria, disponibilidad y throughput real del servidor. - Precision, recall, falsos positivos y falsos negativos, porque requieren conjunto de verdad o evaluacion manual. - Error absoluto entre registros esperados y obtenidos, porque requiere valor esperado externo. - Repetibilidad estadistica de tiempos, porque hoy no se guarda serie historica de tiempos. ## 9. Ecuaciones de metricas Tiempo promedio de respuesta: \[ T_{prom} = \frac{1}{n}\sum_{i=1}^{n}T_i \] Tiempo maximo: \[ T_{max} = \max(T_i) \] Throughput: \[ Th = \frac{N_{consultas}}{T_{total}} \] Tasa de fallos: \[ TF = \frac{N_{fallos}}{N_{consultas}} \times 100 \] Porcentaje de consultas exitosas: \[ CE = \frac{N_{exitosas}}{N_{consultas}} \times 100 \] Error absoluto: \[ EA = |R_{esperado} - R_{obtenido}| \] Error porcentual: \[ EP = \frac{|R_{esperado} - R_{obtenido}|}{R_{esperado}} \times 100 \] Precision: \[ Precision = \frac{VP}{VP + FP} \] Recall: \[ Recall = \frac{VP}{VP + FN} \] Repetibilidad temporal: \[ Rep = 1 - \frac{\sigma_T}{\mu_T} \] Registros procesados por segundo en sincronizacion: \[ RPS = \frac{N_{persistidos}}{T_{sync}} \] Disponibilidad: \[ A = \frac{T_{activo}}{T_{total}} \times 100 \] ## 10. Archivos, funciones y endpoints importantes - Fuente/configuracion: `backend/APIinvima.json`; `backend/APIinvima.js` (`buildInvimaUrl`, `fetchInvimaDataset`). - Adaptacion y transformacion: `backend/invima/adapters.js` (`cleanText`, `parseDateFlexible`, `buildUid`, `baseRecord`, `invimaAdapters`). - Tablas e indices: `backend/invima/service.js` (`ensureInvimaTables`); `backend/comando.sql`. - Sincronizacion: `backend/invima/service.js` (`syncOneDataset`, `syncInvimaDatasets`, `persistBatch`); `backend/invima/sync-job-manager.js`. - Busqueda/filtros: `backend/invima/service.js` (`parseCatalogFilters`, `buildCatalogWhere`, `runCatalogQuery`, `getInvimaCatalog`). - Exportacion: `backend/invima/service.js` (`getInvimaCatalogForExport`, `getInvimaCatalogForExportChunk`, `toCsv`); `backend/src/routes/invima.routes.js`. - Endpoints: `GET /api/invima/datasets`, `GET /api/invima/categorias`, `GET /api/invima/filtros`, `GET /api/invima/sync/stats`, `GET /api/invima/sync/job`, `POST /api/invima/sync`, `GET /api/invima/catalogo`, `GET /api/invima/catalogo/export`. - Frontend de consulta: `semillero/src/app/core/services/invima.service.ts`; `semillero/src/app/features/invima/catalog-page.component.ts`. - Frontend de sincronizacion: `semillero/src/app/features/invima/sync-page.component.ts`. - Manejo de errores: `backend/src/middlewares/error.middleware.js`. - Validaciones basicas: `clampInt`, `parseListParam`, `parseCatalogFilters`, `requireAuth`. No se encontro instrumentacion persistente de logs de consulta, tiempos por endpoint o metricas de CPU/memoria. Los logs existentes son `console.warn`, `console.error` y mensajes de arranque. ## 11. Texto academico en tercera persona La plataforma implementa un modelo computacional de integracion y consulta de datos abiertos del INVIMA basado en una arquitectura de extraccion, normalizacion, almacenamiento e interrogacion sobre PostgreSQL. Los datos se obtienen desde datasets publicados en la API Socrata de datos.gov.co, configurados mediante identificadores de recurso. Cada conjunto de datos posee una estructura original potencialmente diferente, por lo cual el sistema aplica adaptadores especificos que transforman las filas crudas en un esquema comun de catalogo. El modelo distingue dos niveles de almacenamiento. El primero conserva la representacion cruda de la fuente en formato JSONB, permitiendo trazabilidad frente al origen. El segundo corresponde a una relacion normalizada denominada `invima_api_catalog`, en la cual se integran atributos comunes como registro sanitario, expediente, producto, titular, estado del registro, fechas, modalidad, grupo, categoria, fabricante, importador y vigencia. Esta relacion constituye el dominio principal de consulta. Formalmente, el catalogo puede representarse como una relacion \(R\) definida sobre dominios de producto, registro, titular, estado, categoria, fechas y fuente. El proceso de consulta corresponde a una operacion de seleccion parametrizada por criterios introducidos por el usuario. Dichos criterios incluyen texto libre, categorias, datasets, estados, modalidades, grupos, rangos de fechas y vigencia. La seleccion se complementa con ordenamiento, ranking textual y paginacion, por lo que la consulta puede expresarse como \(Q(R,c,limit,offset)=\pi_{limit,offset}(\tau_{orden}(\sigma_{P_c}(R)))\). Cuando el usuario introduce un nombre de producto o dispositivo medico, este actua como variable independiente de busqueda. El resultado no es un valor unico, sino un conjunto de registros relacionados que contienen variables dependientes tales como registro sanitario, expediente, titular, estado, categoria, fechas de expedicion y vencimiento, modalidad, grupo, fabricante, importador, fuente y vigencia. Por tanto, la dependencia puede modelarse como una funcion multivaluada \(g(x)\), donde \(x\) representa el producto consultado y \(g(x)\) retorna el subconjunto de filas del catalogo asociadas al criterio. La transformacion de datos puede formalizarse como \(f_d: R_{crudo,d} \rightarrow R_{catalogo}\). Para cada fila cruda, el sistema ejecuta limpieza textual, normalizacion de fechas, mapeo de columnas segun el dataset, clasificacion canonica, construccion de texto de busqueda e integracion transaccional en la base de datos. Esta formulacion refleja el comportamiento real del codigo y permite describir la plataforma como un modelo reproducible de consulta de datos abiertos, con trazabilidad desde la fuente original hasta la respuesta presentada al usuario. Para evaluar el modelo se recomienda medir tiempo de respuesta, throughput, tasa de fallos, cantidad de registros procesados por segundo, cantidad de resultados por consulta, consistencia ante consultas repetidas, precision y recall. El codigo actual permite medir conteos de registros, avance de sincronizacion, datasets fallidos, paginas procesadas, vigentes, vencidos y total de resultados. No obstante, las metricas de rendimiento temporal, consumo de recursos, precision y recall requieren instrumentacion adicional y, en algunos casos, conjuntos de referencia validados. ## 12. Resultados actuales medidos en la base local Fecha de medicion: 2026-07-07. Ultima actualizacion registrada en los datos: 2026-07-01 01:05:22 UTC. ### Volumen y clasificacion - Registros procesados y normalizados en `invima_api_catalog`: 1 407 694. - Registros crudos preservados en `invima_api_raw`: 1 407 694. - Datasets con datos: 8. - Categorias canonicas por `categoria_slug`: 21. - Etiquetas de categoria visibles por `categoria_canonica`: 28. - Registros vigentes: 751 208. - Registros vencidos: 632 435. - Estados distintos: 48. - Modalidades distintas: 48. - Grupos regulatorios distintos: 16. ### Distribucion por dataset | Dataset | Registros | |---|---:| | `rs_nso` | 1 085 799 | | `dispositivos` | 189 604 | | `cum_vigentes` | 66 501 | | `medicamentos` | 54 047 | | `suplementos` | 6 091 | | `homeopaticos` | 3 477 | | `fitoterapeuticos` | 2 127 | | `vacunas` | 48 | ### Dispositivos y equipos medicos Para el dataset `dispositivos`, el sistema contiene 189 604 registros consultables. El campo `grupo` organiza esos registros en cuatro grupos regulatorios: | Grupo | Registros | |---|---:| | MEDICO QUIRURGICOS | 91 826 | | REACTIVO DIAGNOSTICO | 68 529 | | REACTIVOS IN VITRO | 29 219 | | ODONTOLOGICOS | 30 | Estados principales en `dispositivos`: | Estado | Registros | |---|---:| | Vigente | 125 117 | | Vencido | 47 482 | | Cancelado | 5 926 | | Perdida Fuerza Ejec | 4 530 | | En Estudio | 2 596 | ### Demora de consultas SQL Las demoras se midieron con `EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON)` sobre PostgreSQL local. Los tiempos incluyen planeacion y ejecucion de SQL, no latencia HTTP ni renderizado Angular. | Consulta SQL representativa | Planeacion | Ejecucion | Total SQL | |---|---:|---:|---:| | `COUNT(*)` total de `invima_api_catalog` | 8.20 ms | 187.55 ms | 195.75 ms | | Catalogo general paginado, `LIMIT 25` | 52.88 ms | 6 160.19 ms | 6 213.06 ms | | Conteo de `dispositivos` | 0.12 ms | 149.81 ms | 149.94 ms | | Conteo de `dispositivos` vigentes | 6.41 ms | 287.12 ms | 293.53 ms | | Filtro `dispositivos` vigentes, `LIMIT 25` | 1.66 ms | 375.79 ms | 377.45 ms | | Busqueda textual real del backend para `marcapasos`, `LIMIT 25` | 82.85 ms | 40 427.49 ms | 40 510.33 ms | | Conteo de busqueda textual real para `marcapasos` | 0.27 ms | 37 825.12 ms | 37 825.38 ms | Como `runCatalogQuery` ejecuta primero `COUNT(*)` y despues la consulta paginada, una busqueda textual como `marcapasos` puede tardar aproximadamente 78.34 s solo en SQL si se suman conteo y pagina de resultados. En contraste, una consulta filtrada por dataset y vigencia en `dispositivos` tarda aproximadamente 0.67 s en SQL al sumar conteo y pagina. Tambien se midio la busqueda FTS pura, sin los `OR ILIKE` adicionales del backend: | Consulta FTS pura | Resultados | Tiempo observado | |---|---:|---:| | `marcapasos` | 420 | 92.98 ms | | `vacuna` | 612 | 7.27 ms | Esto muestra que el indice FTS funciona de forma eficiente, pero la condicion real del backend combina FTS con varios `ILIKE`, lo que puede producir exploraciones mas costosas. ### SQL base usado para los conteos ```sql SELECT COUNT(*)::bigint AS catalog_rows, COUNT(DISTINCT source_dataset_key)::int AS datasets_with_data, COUNT(DISTINCT categoria_slug)::int AS categories, COUNT(*) FILTER ( WHERE (COALESCE(estado_registro, '') ILIKE '%vigente%' OR COALESCE(vigencia, '') ILIKE '%vigente%') AND COALESCE(estado_registro, '') NOT ILIKE '%no vigente%' AND COALESCE(vigencia, '') NOT ILIKE '%no vigente%' )::bigint AS vigentes, COUNT(*) FILTER ( WHERE COALESCE(estado_registro, '') ILIKE '%vencido%' OR COALESCE(vigencia, '') ILIKE '%vencido%' )::bigint AS vencidos, MAX(updated_at) AS max_updated_at FROM invima_api_catalog; ``` ```sql SELECT grupo, COUNT(*)::bigint AS total FROM invima_api_catalog WHERE source_dataset_key = 'dispositivos' AND grupo IS NOT NULL AND grupo <> '' GROUP BY grupo ORDER BY total DESC; ``` ### Parrafo de resultados actualizado Como resultado de la medicion actual, el modelo computacional consolido 1 407 694 registros procesados y normalizados en la tabla `invima_api_catalog`, manteniendo igual numero de registros crudos en `invima_api_raw` para trazabilidad con la fuente original. Para el conjunto de dispositivos medicos y otras tecnologias, integro 189 604 registros consultables, distribuidos en cuatro grupos regulatorios principales: medico quirurgicos, reactivo diagnostico, reactivos in vitro y odontologicos. En el catalogo general, el sistema clasifico 751 208 registros vigentes y 632 435 registros vencidos, organizados en 21 categorias canonicas y ocho datasets oficiales. Las pruebas SQL mostraron tiempos de consulta de 195.75 ms para el conteo total del catalogo, 377.45 ms para recuperar una pagina de dispositivos vigentes y 40.51 s para una busqueda textual completa con las condiciones reales del backend. Estos resultados evidencian la capacidad del modelo para estructurar grandes volumenes de datos regulatorios y tambien muestran que la busqueda textual combinada con condiciones `ILIKE` requiere optimizacion adicional para mejorar el rendimiento.