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); });