Files
website/scripts/groceries-search-eval.ts
Stan Runge 60370d4572
All checks were successful
Deploy production / deploy (push) Successful in 1m13s
groceries: relevance-ranked search (plain/basic products first)
- search.ts: score on the core name (brand/size stripped): exact or query+base suffix
  (kipdij+filet), head-final compounds, prefix, inside; penalties per extra word and per
  flavour/processing word (search-words.ts, editable); bonuses for store brands, the
  query's dominant category, store count, favourites; trigram tiebreak
- groceries.synonyms table (seeded: kipdij/kippendij, pasta → spaghetti, …) expands queries
- list rows show all pack sizes of a group; group picker uses the same ranking
- scripts/groceries-search-eval.ts prints top results + latency; preflight now 8 tables
2026-10-25 20:20:34 +00:00

65 lines
2.4 KiB
TypeScript

// Print the top results for some grocery searches with the relevance ranking (search.ts).
// Read-only. Usage: DB_URL=… bun scripts/groceries-search-eval.ts [query …]
// Uses the synonyms table, or the seed in the migration when the table doesn't exist yet.
import { readdirSync, readFileSync } from 'node:fs';
import postgres from 'postgres';
import {
expand,
queryWords,
rankedGroups,
searchConds,
synonymMap,
type Synonyms
} from '../src/lib/server/groceries/search';
const sql = postgres(process.env.DB_URL!, { max: 2 });
const queries = process.argv.slice(2).length
? process.argv.slice(2)
: ['kipdij', 'melk', 'rijst', 'tofu', 'kaas', 'eieren', 'pasta'];
let synonyms: Synonyms;
try {
synonyms = synonymMap(await sql`select term, synonym from groceries.synonyms`);
} catch {
const dir = readdirSync('drizzle').find((d) => d.endsWith('_groceries_search_synonyms'))!;
const seed = readFileSync(`drizzle/${dir}/migration.sql`, 'utf8');
const rows = [...seed.matchAll(/\('([a-z0-9 -]+)', '([a-z0-9 -]+)'\)/g)].map((m) => ({
term: m[1],
synonym: m[2]
}));
synonyms = synonymMap(rows);
console.log(`(synonyms table missing; using the ${rows.length} from the migration seed)`);
}
const cats = await sql<
{ id: number; parent_id: number | null }[]
>`select id, parent_id from groceries.categories`;
const ctx = { at: Date.now(), synonyms, parent: new Map(cats.map((c) => [c.id, c.parent_id])) };
for (const q of queries) {
const words = expand(queryWords(q), synonyms);
const where = [
sql`g.merged_into_id is null`,
sql`(g.item_count > 0 or g.favorite)`,
...searchConds(sql, words)
].reduce((a, c) => sql`${a} and ${c}`);
const t = performance.now();
const ranked = await rankedGroups(sql, where, q, words, { ctx });
const ids = ranked.map((r) => r.id);
const ms = performance.now() - t;
const top = ids.slice(0, Number(process.env.TOP) || 5);
const rows = await sql<{ id: number; name: string; brand: string | null; store_count: number }[]>`
select id, name, brand, store_count from groceries.groups where id = any(${top}::int[])`;
console.log(`\n${q}: ${ids.length} candidates, ${ms.toFixed(0)} ms`);
for (const id of top) {
const r = rows.find((x) => x.id === id)!;
const sc = ranked.find((x) => x.id === id)!;
console.log(
` ${r.name} | ${r.brand ?? '-'} | ${r.store_count} stores | #${r.id}` +
(process.env.DEBUG
? ` | score ${sc.score} text ${sc.text} pen ${sc.penalty} cat ${sc.categoryId}`
: '')
);
}
}
await sql.end();