Compare commits

..

4 commits

Author SHA1 Message Date
5b132db15e el algoritmo ya no es tan básico 2024-09-16 21:59:15 -03:00
5830cca9c5 fts con postgres 2024-09-16 21:58:58 -03:00
841a1412a1 optimize search query + fix sentry 2024-09-16 21:53:47 -03:00
9fced663ff usar proxy para datos.produccion.gob.ar 2024-09-16 21:24:26 -03:00
8 changed files with 501 additions and 102 deletions

View file

@ -27,10 +27,8 @@ jobs:
B2_BUCKET_NAME: ${{ secrets.B2_BUCKET_NAME }} B2_BUCKET_NAME: ${{ secrets.B2_BUCKET_NAME }}
B2_BUCKET_KEY_ID: ${{ secrets.B2_BUCKET_KEY_ID }} B2_BUCKET_KEY_ID: ${{ secrets.B2_BUCKET_KEY_ID }}
B2_BUCKET_KEY: ${{ secrets.B2_BUCKET_KEY }} B2_BUCKET_KEY: ${{ secrets.B2_BUCKET_KEY }}
DATOS_PRODUCCION_GOB_AR: https://proxy-datos-produccion-gob-ar.nulo.in
run: | run: |
# usar un servidor especifico porque parece que a veces
# bloquean el acceso desde afuera del país
sudo echo "190.2.53.185 datos.produccion.gob.ar" | sudo tee -a /etc/hosts
cd sepa cd sepa
bun install --frozen-lockfile bun install --frozen-lockfile
bun archiver.ts bun archiver.ts

View file

@ -23,6 +23,11 @@ const B2_BUCKET_NAME = checkEnvVariable("B2_BUCKET_NAME");
const B2_BUCKET_KEY_ID = checkEnvVariable("B2_BUCKET_KEY_ID"); const B2_BUCKET_KEY_ID = checkEnvVariable("B2_BUCKET_KEY_ID");
const B2_BUCKET_KEY = checkEnvVariable("B2_BUCKET_KEY"); const B2_BUCKET_KEY = checkEnvVariable("B2_BUCKET_KEY");
const DATOS_PRODUCCION_GOB_AR =
process.env.DATOS_PRODUCCION_GOB_AR || "https://datos.produccion.gob.ar";
const processUrl = (url: string) =>
url.replace(/^https:\/\/datos\.produccion\.gob\.ar/, DATOS_PRODUCCION_GOB_AR);
const s3 = new S3Client({ const s3 = new S3Client({
endpoint: "https://s3.us-west-004.backblazeb2.com", endpoint: "https://s3.us-west-004.backblazeb2.com",
region: "us-west-004", region: "us-west-004",
@ -34,7 +39,10 @@ const s3 = new S3Client({
async function getRawDatasetInfo(attempts = 0) { async function getRawDatasetInfo(attempts = 0) {
try { try {
return await $`curl -L https://datos.produccion.gob.ar/api/3/action/package_show?id=sepa-precios`.json(); const url = processUrl(
"https://datos.produccion.gob.ar/api/3/action/package_show?id=sepa-precios"
);
return await $`curl -L ${url}`.json();
} catch (error) { } catch (error) {
if (attempts >= 4) { if (attempts >= 4) {
console.error(`❌ Error fetching dataset info`, error); console.error(`❌ Error fetching dataset info`, error);
@ -137,7 +145,8 @@ for (const resource of datasetInfo.result.resources) {
console.info(dir); console.info(dir);
try { try {
const zip = join(dir, "zip"); const zip = join(dir, "zip");
await $`curl --retry 8 --retry-delay 5 --retry-all-errors -L -o ${zip} ${resource.url}`; const url = processUrl(resource.url);
await $`curl --retry 8 --retry-delay 5 --retry-all-errors -L -o ${zip} ${url}`;
await $`unzip ${zip} -d ${dir}`; await $`unzip ${zip} -d ${dir}`;
await rm(zip); await rm(zip);

View file

@ -1,4 +1,4 @@
import { max, relations } from "drizzle-orm"; import { max, relations, sql } from "drizzle-orm";
import { import {
pgTable, pgTable,
integer, integer,
@ -179,5 +179,11 @@ export const productos_descripcion_index = pgTable(
id_producto: bigint("id_producto", { mode: "bigint" }), id_producto: bigint("id_producto", { mode: "bigint" }),
productos_descripcion: text("productos_descripcion").unique(), productos_descripcion: text("productos_descripcion").unique(),
productos_marca: text("productos_marca"), productos_marca: text("productos_marca"),
} },
(table) => ({
// https://orm.drizzle.team/learn/guides/postgresql-full-text-search
tableSearchIndex: index(
"productos_descripcion_index_search_descripcion"
).using("gin", sql`to_tsvector('spanish', ${table.productos_descripcion})`),
})
); );

View file

@ -0,0 +1 @@
CREATE INDEX IF NOT EXISTS "productos_descripcion_index_search_descripcion" ON "productos_descripcion_index" USING gin (to_tsvector('spanish', "productos_descripcion"));

View file

@ -0,0 +1,446 @@
{
"id": "36a04e5e-b070-4b89-96ce-e9431c2f199b",
"prevId": "7ea9c123-4d12-4f0b-9452-e8952462fbf8",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.datasets": {
"name": "datasets",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": false
},
"date": {
"name": "date",
"type": "date",
"primaryKey": false,
"notNull": false
},
"id_comercio": {
"name": "id_comercio",
"type": "integer",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"datasets_name_key": {
"name": "datasets_name_key",
"nullsNotDistinct": false,
"columns": [
"name"
]
}
}
},
"public.precios": {
"name": "precios",
"schema": "",
"columns": {
"id_dataset": {
"name": "id_dataset",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"id_comercio": {
"name": "id_comercio",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"id_bandera": {
"name": "id_bandera",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"id_sucursal": {
"name": "id_sucursal",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"id_producto": {
"name": "id_producto",
"type": "bigint",
"primaryKey": false,
"notNull": false
},
"productos_ean": {
"name": "productos_ean",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"productos_descripcion": {
"name": "productos_descripcion",
"type": "text",
"primaryKey": false,
"notNull": false
},
"productos_cantidad_presentacion": {
"name": "productos_cantidad_presentacion",
"type": "numeric(10, 2)",
"primaryKey": false,
"notNull": false
},
"productos_unidad_medida_presentacion": {
"name": "productos_unidad_medida_presentacion",
"type": "text",
"primaryKey": false,
"notNull": false
},
"productos_marca": {
"name": "productos_marca",
"type": "text",
"primaryKey": false,
"notNull": false
},
"productos_precio_lista": {
"name": "productos_precio_lista",
"type": "numeric(10, 2)",
"primaryKey": false,
"notNull": false
},
"productos_precio_referencia": {
"name": "productos_precio_referencia",
"type": "numeric(10, 2)",
"primaryKey": false,
"notNull": false
},
"productos_cantidad_referencia": {
"name": "productos_cantidad_referencia",
"type": "numeric(10, 2)",
"primaryKey": false,
"notNull": false
},
"productos_unidad_medida_referencia": {
"name": "productos_unidad_medida_referencia",
"type": "text",
"primaryKey": false,
"notNull": false
},
"productos_precio_unitario_promo1": {
"name": "productos_precio_unitario_promo1",
"type": "numeric(10, 2)",
"primaryKey": false,
"notNull": false
},
"productos_leyenda_promo1": {
"name": "productos_leyenda_promo1",
"type": "text",
"primaryKey": false,
"notNull": false
},
"productos_precio_unitario_promo2": {
"name": "productos_precio_unitario_promo2",
"type": "numeric(10, 2)",
"primaryKey": false,
"notNull": false
},
"productos_leyenda_promo2": {
"name": "productos_leyenda_promo2",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {
"idx_precios_id_producto": {
"name": "idx_precios_id_producto",
"columns": [
{
"expression": "id_producto",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"idx_precios_id_producto_id_dataset": {
"name": "idx_precios_id_producto_id_dataset",
"columns": [
{
"expression": "id_producto",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "id_dataset",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"precios_id_dataset_datasets_id_fk": {
"name": "precios_id_dataset_datasets_id_fk",
"tableFrom": "precios",
"tableTo": "datasets",
"columnsFrom": [
"id_dataset"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"public.productos_descripcion_index": {
"name": "productos_descripcion_index",
"schema": "",
"columns": {
"id_producto": {
"name": "id_producto",
"type": "bigint",
"primaryKey": false,
"notNull": false
},
"productos_descripcion": {
"name": "productos_descripcion",
"type": "text",
"primaryKey": false,
"notNull": false
},
"productos_marca": {
"name": "productos_marca",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {
"productos_descripcion_index_search_descripcion": {
"name": "productos_descripcion_index_search_descripcion",
"columns": [
{
"expression": "to_tsvector('spanish', \"productos_descripcion\")",
"asc": true,
"isExpression": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "gin",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"productos_descripcion_index_productos_descripcion_unique": {
"name": "productos_descripcion_index_productos_descripcion_unique",
"nullsNotDistinct": false,
"columns": [
"productos_descripcion"
]
}
}
},
"public.sucursales": {
"name": "sucursales",
"schema": "",
"columns": {
"id_dataset": {
"name": "id_dataset",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"id_comercio": {
"name": "id_comercio",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"id_bandera": {
"name": "id_bandera",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"id_sucursal": {
"name": "id_sucursal",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"sucursales_nombre": {
"name": "sucursales_nombre",
"type": "text",
"primaryKey": false,
"notNull": false
},
"sucursales_tipo": {
"name": "sucursales_tipo",
"type": "text",
"primaryKey": false,
"notNull": false
},
"sucursales_calle": {
"name": "sucursales_calle",
"type": "text",
"primaryKey": false,
"notNull": false
},
"sucursales_numero": {
"name": "sucursales_numero",
"type": "text",
"primaryKey": false,
"notNull": false
},
"sucursales_latitud": {
"name": "sucursales_latitud",
"type": "numeric",
"primaryKey": false,
"notNull": false
},
"sucursales_longitud": {
"name": "sucursales_longitud",
"type": "numeric",
"primaryKey": false,
"notNull": false
},
"sucursales_observaciones": {
"name": "sucursales_observaciones",
"type": "text",
"primaryKey": false,
"notNull": false
},
"sucursales_barrio": {
"name": "sucursales_barrio",
"type": "text",
"primaryKey": false,
"notNull": false
},
"sucursales_codigo_postal": {
"name": "sucursales_codigo_postal",
"type": "text",
"primaryKey": false,
"notNull": false
},
"sucursales_localidad": {
"name": "sucursales_localidad",
"type": "text",
"primaryKey": false,
"notNull": false
},
"sucursales_provincia": {
"name": "sucursales_provincia",
"type": "text",
"primaryKey": false,
"notNull": false
},
"sucursales_lunes_horario_atencion": {
"name": "sucursales_lunes_horario_atencion",
"type": "text",
"primaryKey": false,
"notNull": false
},
"sucursales_martes_horario_atencion": {
"name": "sucursales_martes_horario_atencion",
"type": "text",
"primaryKey": false,
"notNull": false
},
"sucursales_miercoles_horario_atencion": {
"name": "sucursales_miercoles_horario_atencion",
"type": "text",
"primaryKey": false,
"notNull": false
},
"sucursales_jueves_horario_atencion": {
"name": "sucursales_jueves_horario_atencion",
"type": "text",
"primaryKey": false,
"notNull": false
},
"sucursales_viernes_horario_atencion": {
"name": "sucursales_viernes_horario_atencion",
"type": "text",
"primaryKey": false,
"notNull": false
},
"sucursales_sabado_horario_atencion": {
"name": "sucursales_sabado_horario_atencion",
"type": "text",
"primaryKey": false,
"notNull": false
},
"sucursales_domingo_horario_atencion": {
"name": "sucursales_domingo_horario_atencion",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"sucursales_id_dataset_datasets_id_fk": {
"name": "sucursales_id_dataset_datasets_id_fk",
"tableFrom": "sucursales",
"tableTo": "datasets",
"columnsFrom": [
"id_dataset"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"sucursales_id_dataset_id_comercio_id_bandera_id_sucursal_key": {
"name": "sucursales_id_dataset_id_comercio_id_bandera_id_sucursal_key",
"nullsNotDistinct": false,
"columns": [
"id_dataset",
"id_comercio",
"id_bandera",
"id_sucursal"
]
}
}
}
},
"enums": {},
"schemas": {},
"sequences": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View file

@ -29,6 +29,13 @@
"when": 1726283266814, "when": 1726283266814,
"tag": "0003_strong_bucky", "tag": "0003_strong_bucky",
"breakpoints": true "breakpoints": true
},
{
"idx": 4,
"version": "7",
"when": 1726534597731,
"tag": "0004_mushy_ultragirl",
"breakpoints": true
} }
] ]
} }

View file

@ -1,22 +1,10 @@
import { db } from '$lib/server/db'; import { db } from '$lib/server/db';
import { sql } from 'drizzle-orm'; import * as schema from '$lib/server/db/schema';
import { ilike, or, sql } from 'drizzle-orm';
import type { PageServerLoad } from './$types'; import type { PageServerLoad } from './$types';
import * as Sentry from '@sentry/sveltekit'; import * as Sentry from '@sentry/sveltekit';
export const load: PageServerLoad = async ({ params, setHeaders }) => { export const load: PageServerLoad = async ({ params, setHeaders }) => {
// const latestDatasetsSq = db.$with('latest_datasets').as(
// db.select({
// id: datasets.id,
// }).from(datasets)
// .innerJoin(
// db.select({
// id_comercio: datasets.id_comercio,
// max_date: max(datasets.date),
// }).from(d2).groupBy(datasets.id_comercio),
// and(eq(datasets.id_comercio, d2.id_comercio), eq(datasets.date, d2.max_date))
// // 'datasets.id_comercio = latest_datasets.id_comercio AND datasets.date = latest_datasets.max_date'
// ))
const query = params.query const query = params.query
.replaceAll(/á/giu, 'a') .replaceAll(/á/giu, 'a')
.replaceAll(/é/giu, 'e') .replaceAll(/é/giu, 'e')
@ -24,8 +12,12 @@ export const load: PageServerLoad = async ({ params, setHeaders }) => {
.replaceAll(/ó/giu, 'o') .replaceAll(/ó/giu, 'o')
.replaceAll(/ú/giu, 'u') .replaceAll(/ú/giu, 'u')
.replaceAll(/ñ/giu, 'n'); .replaceAll(/ñ/giu, 'n');
const productosQuery = sql` const productosQuery = db
SELECT id_producto, productos_descripcion, productos_marca, .select({
id_producto: schema.productos_descripcion_index.id_producto,
productos_descripcion: schema.productos_descripcion_index.productos_descripcion,
productos_marca: schema.productos_descripcion_index.productos_marca,
in_datasets_count: sql<number>`
(WITH latest_datasets AS ( (WITH latest_datasets AS (
SELECT d1.id SELECT d1.id
FROM datasets d1 FROM datasets d1
@ -37,32 +29,31 @@ export const load: PageServerLoad = async ({ params, setHeaders }) => {
)SELECT COUNT(DISTINCT p.id_dataset) as dataset_count )SELECT COUNT(DISTINCT p.id_dataset) as dataset_count
FROM precios p FROM precios p
JOIN latest_datasets ld ON p.id_dataset = ld.id JOIN latest_datasets ld ON p.id_dataset = ld.id
WHERE p.id_producto = index.id_producto) as in_datasets_count WHERE p.id_producto = productos_descripcion_index.id_producto)`.as('in_datasets_count')
FROM productos_descripcion_index index })
WHERE productos_descripcion ILIKE ${`%${query}%`} .from(schema.productos_descripcion_index)
ORDER BY in_datasets_count desc .where(
LIMIT 100 or(
`; sql`to_tsvector('spanish', ${schema.productos_descripcion_index.productos_descripcion}) @@ to_tsquery('spanish', ${query})`,
ilike(schema.productos_descripcion_index.productos_marca, `%${query}%`)
)
)
.orderBy(sql`in_datasets_count desc`)
.limit(100);
const productos = await Sentry.startSpan( const productos = await Sentry.startSpan(
{ {
op: 'db.query', op: 'db.query',
name: productosQuery, name: productosQuery.toSQL().sql,
data: { 'db.system': 'postgresql' } data: { 'db.system': 'postgresql' }
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any, } as any,
() => () => productosQuery
db.execute<{
id_producto: string;
productos_descripcion: string;
productos_marca: string | null;
in_datasets_count: number;
}>(productosQuery)
); );
const collapsedProductos = productos.reduce( const collapsedProductos = productos.reduce(
(acc, producto) => { (acc, producto) => {
const existingProduct = acc.find((p) => p.id_producto === producto.id_producto); const existingProduct = acc.find((p) => BigInt(p.id_producto) === producto.id_producto);
if (existingProduct) { if (existingProduct) {
existingProduct.descriptions.push(producto.productos_descripcion); existingProduct.descriptions.push(producto.productos_descripcion!);
if (producto.productos_marca) existingProduct.marcas.add(producto.productos_marca); if (producto.productos_marca) existingProduct.marcas.add(producto.productos_marca);
existingProduct.in_datasets_count = Math.max( existingProduct.in_datasets_count = Math.max(
existingProduct.in_datasets_count, existingProduct.in_datasets_count,
@ -70,8 +61,8 @@ WHERE p.id_producto = index.id_producto) as in_datasets_count
); );
} else { } else {
acc.push({ acc.push({
id_producto: producto.id_producto, id_producto: producto.id_producto!.toString(),
descriptions: [producto.productos_descripcion], descriptions: [producto.productos_descripcion!],
marcas: new Set(producto.productos_marca ? [producto.productos_marca] : []), marcas: new Set(producto.productos_marca ? [producto.productos_marca] : []),
in_datasets_count: producto.in_datasets_count in_datasets_count: producto.in_datasets_count
}); });
@ -90,63 +81,5 @@ WHERE p.id_producto = index.id_producto) as in_datasets_count
'Cache-Control': 'public, max-age=600' 'Cache-Control': 'public, max-age=600'
}); });
// 'latest_datasets',
// sql`
// WITH latest_datasets AS (
// SELECT d1.id
// FROM datasets d1
// JOIN (
// SELECT id_comercio, MAX(date) as max_date
// FROM datasets
// GROUP BY id_comercio
// ) d2 ON d1.id_comercio = d2.id_comercio AND d1.date = d2.max_date
// )`
// .select({
// id_producto: productos_descripcion_index.id_producto,
// productos_descripcion: productos_descripcion_index.productos_descripcion,
// productos_marca: productos_descripcion_index.productos_marca,
// })
// .from(productos_descripcion_index)
// .where(ilike(productos_descripcion_index.productos_descripcion, `%${query}%`))
// .orderBy(sql`
// WITH latest_datasets AS (
// SELECT d1.id
// FROM datasets d1
// JOIN (
// SELECT id_comercio, MAX(date) as max_date
// FROM datasets
// GROUP BY id_comercio
// ) d2 ON d1.id_comercio = d2.id_comercio AND d1.date = d2.max_date
// )
// SELECT COUNT(DISTINCT p.id_dataset) as dataset_count
// FROM precios p
// JOIN latest_datasets ld ON p.id_dataset = ld.id
// WHERE p.id_producto = ${productos_descripcion_index.id_producto}`);
return { productos, collapsedProductos, query }; return { productos, collapsedProductos, query };
// const precios = await sql<
// {
// id_producto: string;
// productos_precio_lista: number;
// productos_descripcion: string;
// }[]
// >`
// WITH latest_prices AS (
// SELECT DISTINCT ON (id_comercio, id_producto)
// id_comercio,
// id_producto,
// productos_precio_lista,
// productos_descripcion
// FROM precios
// )
// SELECT
// id_producto,
// productos_precio_lista,
// productos_descripcion
// FROM latest_prices
// WHERE productos_descripcion ILIKE ${`%${query}%`}
// `;
// return {
// precios
// };
}; };

View file

@ -34,9 +34,8 @@
<h1 class="my-2 text-2xl font-bold">Resultados para "{data.query}"</h1> <h1 class="my-2 text-2xl font-bold">Resultados para "{data.query}"</h1>
{#if data.collapsedProductos.length === 0} {#if data.collapsedProductos.length === 0}
<p class="my-2 text-gray-600"> <p class="my-2 text-gray-600">
No se encontraron resultados para "{data.query}". Tené en cuenta que actualmente, el algoritmo No se encontraron resultados para "{data.query}". Probá buscando palabras clave como
de búsqueda es muy básico. Probá buscando palabras clave como "alfajor", "ketchup" o "alfajor", "ketchup" o "lenteja".
"lenteja".
</p> </p>
{:else} {:else}
{#each data.collapsedProductos as producto} {#each data.collapsedProductos as producto}