groceries: relevance-ranked search (plain/basic products first)
All checks were successful
Deploy production / deploy (push) Successful in 1m13s
All checks were successful
Deploy production / deploy (push) Successful in 1m13s
- 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
This commit is contained in:
64
scripts/groceries-search-eval.ts
Normal file
64
scripts/groceries-search-eval.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
// 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();
|
||||
Reference in New Issue
Block a user