# Spec: 06 · Fruit Search **Taiga Story:** #6 · `06-fruit-search` **Status:** Ready **Branch:** `feature/06-fruit-search` **Depends on:** `02-manage-fruits` (fruits + fruit_synonyms) --- ## 1. Goal Improve the fruit list search: users can filter by name/synonym text and optionally by type. Combined-type aliases (e.g. "Birnen- und Quittensorten") match both constituent types. --- ## 2. Background / Scope / Context / Constraints - Target component: `FruitList.vue` (search bar already exists as placeholder from story #2). - Search is case-insensitive, substring match on `fruits.name` OR any `fruit_synonyms.synonym`. - Type filter is optional; when set, filters to exactly that enum value OR to its alias group. - Combined-type aliases (not in the enum — only in search UI): | Alias | Matches enum values | |-------|-------------------| | Birnen- und Quittensorten | `Birnensorten`, `Quittensorten` | | Aprikosen und Pfirsiche | `Aprikosen`, `Pfirsiche` | | Mirabellen und Reineclauden | `Mirabellen`, `Renekloden` | | Pflaumen und Zwetschen | `Pflaumen`, `Zwetschen` | - Search is client-triggered (on input, debounced) or button-triggered — consistent with existing UX pattern. - API already supports `?name=` and `?type=` query params (placeholder from story #2); this story implements them correctly on the backend. --- ## 3. Necessary Refactorings - `GET /api/v1/fruits` handler: add SQL filter logic for `name` (ILIKE on name + JOIN synonyms) and `type` (exact enum match or multi-value IN for aliases). - `FruitList.vue`: wire up search input + type dropdown to API query params. --- ## 4. Related Data Model No new tables or migrations. Query pattern: ```sql SELECT DISTINCT f.* FROM fruits f LEFT JOIN fruit_synonyms fs ON fs.fruit_id = f.id WHERE ($name = '' OR f.name ILIKE '%' || $name || '%' OR fs.synonym ILIKE '%' || $name || '%') AND ($types IS NULL OR f.fruit_type = ANY($types::fruit_type[])) ORDER BY f.name; ``` `$types` is an array expanded from the selected alias or single type value. --- ## 5. Affected Places / Related Components ### Backend | File | Change | |------|--------| | `backend/internal/repository/fruit_repo.go` | `List(name, types []string)` — parameterized SQL | | `backend/internal/handler/fruit_handler.go` | Parse `?name=` and `?type=` query params; resolve alias → types array | | `backend/internal/domain/fruit.go` | Add alias map constant | ### Frontend | File | Change | |------|--------| | `frontend/src/views/FruitList.vue` | Search text input (debounced 300 ms) + type dropdown (enum values + aliases) | | `frontend/src/api/fruits.ts` | `listFruits({name?, type?})` passes query params | | `frontend/src/stores/fruitStore.ts` | Store search state; trigger reload on change | ### Type Dropdown Options ``` (All) Apfelsorten Birnensorten Quittensorten Birnen- und Quittensorten ← alias Aprikosen Pfirsiche Aprikosen und Pfirsiche ← alias Mirabellen Renekloden Mirabellen und Reineclauden ← alias Pflaumen Zwetschen Pflaumen und Zwetschen ← alias Sauerkirschen Süßkirschen Brombeeren Erdbeeren Himbeeren Johannisbeeren Stachelbeeren Wein ``` --- ## 6. Out of Scope - Full-text search index (pg `tsvector`) — ILIKE sufficient for expected dataset size - Search in comment field - Sorting controls - Pagination (defer to later if needed) --- ## 7. Verification | Check | Expected | |-------|---------| | Search "Boskop" → results include fruit named "Boskop" | Pass | | Search "boskop" (lowercase) | Same results (case-insensitive) | | Search by synonym term | Fruits with matching synonym appear | | Filter "Birnensorten" | Only Birnensorten fruits | | Filter "Birnen- und Quittensorten" | Birnensorten AND Quittensorten fruits | | Combined name + type filter | Intersection of both | | Empty search + no type filter | All fruits | | `go test ./...` — repo list filter tests | Green | --- ## 8. Open Questions | # | Question | Impact | |---|----------|--------| | 1 | Debounce delay: 300 ms sufficient or user preference? | UX | | 2 | Search on `Enter` only vs real-time? | UX | | 3 | Type dropdown: grouped by alias vs flat list? | UX clarity |