kiwi/app/db/migrations/021_recipe_browser_fts.sql
pyr0ball a523cb094e perf(browser): replace LIKE scans with FTS5; cache category counts
- Add migration 021: recipe_browser_fts FTS5 table on category + keywords
  columns, eliminating LIKE '%keyword%' full sequential scans on 3.1M rows
- _count_recipes_for_keywords now uses FTS5 MATCH (O(log N) vs O(N))
- browse_recipes reuses cached count, eliminating the second COUNT(*) scan
  per page request; ORDER BY r.id replaces the unindexed ORDER BY title sort
- Module-level _COUNT_CACHE keyed by (db_path, keywords) means domain-switch
  category counts are computed once per process lifetime

feat(find): dietary preset grid, Big 9 allergen pills, Hard Day Mode surface

- Dietary constraints replaced with toggle-button preset grid (8 options)
  + free-text "Other" field; removes dense freeform text input
- Allergies replaced with Big 9 pill picker (peanuts, tree nuts, shellfish,
  fish, milk, eggs, wheat, soy, sesame) + "Other" for custom entries
- Hard Day Mode surfaced as a standalone aria-pressed button above the
  dietary collapsible; no longer buried inside a collapsed section
- Active-state dot indicators on both collapsibles show filter engagement
  at a glance without expanding

fix(a11y): aria-describedby wiring for wildcard checkbox and tag inputs (#40)

- Persistent hint spans replace placeholder-only instructions for constraint
  and allergy fields (WCAG 3.3.2)

fix(browse): auto-select highest-count category on domain switch (#41)

- Eliminates the 3-decision cold start (domain → category → content)
- Surprise Me button added for zero-decision random navigation
2026-04-08 23:10:48 -07:00

43 lines
1.7 KiB
SQL

-- Migration 021: FTS5 inverted index for the recipe browser (category + keywords).
--
-- The browser domain queries were using LIKE '%keyword%' against category and
-- keywords columns — a leading wildcard prevents any B-tree index use, so every
-- query was a full sequential scan of 3.1M rows. This FTS5 index replaces those
-- scans with O(log N) token lookups.
--
-- Content-table backed: stores only the inverted index, no text duplication.
-- The keywords column is a JSON array; FTS5 tokenises it as plain text, stripping
-- the punctuation, which gives correct per-word matching.
--
-- One-time rebuild cost on 3.1M rows: ~20-40 seconds at first startup.
-- Subsequent startups skip this migration (IF NOT EXISTS guard).
CREATE VIRTUAL TABLE IF NOT EXISTS recipe_browser_fts USING fts5(
category,
keywords,
content=recipes,
content_rowid=id,
tokenize="unicode61"
);
INSERT INTO recipe_browser_fts(recipe_browser_fts) VALUES('rebuild');
CREATE TRIGGER IF NOT EXISTS recipe_browser_fts_ai
AFTER INSERT ON recipes BEGIN
INSERT INTO recipe_browser_fts(rowid, category, keywords)
VALUES (new.id, new.category, new.keywords);
END;
CREATE TRIGGER IF NOT EXISTS recipe_browser_fts_ad
AFTER DELETE ON recipes BEGIN
INSERT INTO recipe_browser_fts(recipe_browser_fts, rowid, category, keywords)
VALUES ('delete', old.id, old.category, old.keywords);
END;
CREATE TRIGGER IF NOT EXISTS recipe_browser_fts_au
AFTER UPDATE ON recipes BEGIN
INSERT INTO recipe_browser_fts(recipe_browser_fts, rowid, category, keywords)
VALUES ('delete', old.id, old.category, old.keywords);
INSERT INTO recipe_browser_fts(rowid, category, keywords)
VALUES (new.id, new.category, new.keywords);
END;