semillero-INVIMA/benchmarks/scripts/validate-search-results.js

205 lines
5.7 KiB
JavaScript

const fs = require('fs');
const path = require('path');
const { createClient, resultsDir, writeJson } = require('./benchmark-lib');
const outFile = path.join(resultsDir, 'validation_search_results.json');
const mode = process.argv[2] || process.env.BENCHMARK_VALIDATION_PHASE || 'after';
const whereSql = `
WHERE (
to_tsvector('spanish', COALESCE(search_text, '')) @@ websearch_to_tsquery('spanish', 'marcapasos')
OR COALESCE(registro_sanitario, '') ILIKE '%marcapasos%'
OR COALESCE(expediente, '') ILIKE '%marcapasos%'
OR COALESCE(producto, '') ILIKE '%marcapasos%'
OR COALESCE(categoria_canonica, '') ILIKE '%marcapasos%'
OR COALESCE(categoria_linea, '') ILIKE '%marcapasos%'
)`;
const countSql = `SELECT COUNT(*)::bigint AS total FROM invima_api_catalog ${whereSql};`;
const pageSql = `
WITH matched AS MATERIALIZED (
SELECT
source_dataset_key,
source_uid,
source_dataset_id,
source_dataset_name,
categoria_linea,
categoria_canonica,
categoria_slug,
expediente,
registro_sanitario,
producto,
titular,
estado_registro,
fecha_expedicion,
fecha_vencimiento,
modalidad,
grupo,
marca,
principio_activo,
forma_farmaceutica,
presentacion_comercial,
atc,
rol_nombre,
rol_tipo,
ciudad_titular,
pais_titular,
fabricante,
importador,
vigencia
FROM invima_api_catalog
${whereSql}
)
SELECT
source_dataset_key,
source_uid,
source_dataset_id,
source_dataset_name,
categoria_linea,
categoria_canonica,
categoria_slug,
expediente,
registro_sanitario,
producto,
titular,
estado_registro,
fecha_expedicion,
fecha_vencimiento,
modalidad,
grupo,
marca,
principio_activo,
forma_farmaceutica,
presentacion_comercial,
atc,
rol_nombre,
rol_tipo,
ciudad_titular,
pais_titular,
fabricante,
importador,
vigencia
FROM matched
ORDER BY fecha_vencimiento DESC NULLS LAST, producto ASC NULLS LAST, source_dataset_key ASC, source_uid ASC
LIMIT 25 OFFSET 0;
`;
const rowId = (row) => `${row.source_dataset_key}|${row.source_uid}`;
const normalizeDate = (value) => {
if (!value) return '';
if (value instanceof Date) return value.toISOString();
return String(value);
};
const orderKey = (row) =>
`${normalizeDate(row.fecha_vencimiento)}|${row.producto || ''}|${row.source_dataset_key}|${row.source_uid}`;
const duplicates = (ids) => {
const seen = new Set();
const dupes = new Set();
for (const id of ids) {
if (seen.has(id)) dupes.add(id);
seen.add(id);
}
return Array.from(dupes);
};
const diff = (before, after) => {
const beforeIds = before.rows.map(rowId);
const afterIds = after.rows.map(rowId);
const beforeOrderKeys = before.rows.map(orderKey);
const afterOrderKeys = after.rows.map(orderKey);
const beforeSet = new Set(beforeIds);
const afterSet = new Set(afterIds);
const missing = beforeIds.filter((id) => !afterSet.has(id));
const additional = afterIds.filter((id) => !beforeSet.has(id));
const orderChanged =
beforeIds.length !== afterIds.length || beforeIds.some((id, index) => id !== afterIds[index]);
const orderKeysChanged =
beforeOrderKeys.length !== afterOrderKeys.length ||
beforeOrderKeys.some((key, index) => key !== afterOrderKeys[index]);
const countBefore = Number(before.count);
const countAfter = Number(after.count);
const validResultSet =
countBefore === countAfter &&
before.rows.length === after.rows.length &&
missing.length === 0 &&
additional.length === 0 &&
duplicates(afterIds).length === 0;
const validStrictOrder = validResultSet && !orderChanged;
const validOrderKeys = validResultSet && !orderKeysChanged;
return {
countBefore,
countAfter,
countMatches: countBefore === countAfter,
rowCountBefore: before.rows.length,
rowCountAfter: after.rows.length,
rowCountMatches: before.rows.length === after.rows.length,
missingIds: missing,
additionalIds: additional,
duplicateIdsBefore: duplicates(beforeIds),
duplicateIdsAfter: duplicates(afterIds),
orderChanged,
orderKeysChanged,
idOrderChangedWithinTies: orderChanged && !orderKeysChanged,
validResultSet,
validStrictOrder,
validOrderKeys,
valid: validResultSet && validOrderKeys,
note:
orderChanged && !orderKeysChanged
? 'Los mismos registros aparecen bajo las mismas claves ORDER BY; cambia el orden interno de empates porque la consulta no incluye un desempate unico.'
: ''
};
};
const capture = async () => {
const client = await createClient();
try {
await client.query('SET statement_timeout = 0');
const count = await client.query(countSql);
const page = await client.query(pageSql);
return {
capturedAt: new Date().toISOString(),
count: Number(count.rows[0]?.total || 0),
rows: page.rows,
ids: page.rows.map(rowId)
};
} finally {
await client.end();
}
};
const main = async () => {
const current = fs.existsSync(outFile)
? JSON.parse(fs.readFileSync(outFile, 'utf8'))
: {
query: 'marcapasos',
validationSql: {
count: countSql.trim(),
page: pageSql.trim()
}
};
if (mode === 'before') {
current.before = await capture();
delete current.after;
delete current.comparison;
} else {
if (!current.before) {
throw new Error('No existe referencia before. Ejecute: node benchmarks/scripts/validate-search-results.js before');
}
current.after = await capture();
current.comparison = diff(current.before, current.after);
}
writeJson(outFile, current);
console.log(JSON.stringify(current.comparison || { beforeCaptured: Boolean(current.before) }, null, 2));
};
main().catch((error) => {
console.error(error);
process.exit(1);
});