[agent] feat(recipe): add --in flag for field-scoped recipe search #1

Open
Lauria wants to merge 1 commit from agent/search-by-ingredient into main
Owner

What

Add a --in {title,ingredients,both} flag to mealie-cli recipe search.
Until now, recipe search was a thin pass-through to Mealie's
/api/recipes?search=… endpoint, which is fuzzy and unreliable (it
returns 100 results even when only a handful contain the query, and
it doesn't search ingredient lines at all). The new flag makes the
search deterministic and adds ingredient-level matching.

Why

User case: "find me recipes that use ground beef" (recettes qui
utilisent du boeuf haché). The previous recipe search boeuf
returned 100 results, mostly unrelated. Going to the API and
post-filtering on each recipe's recipeIngredient[] list gave the
real answer (16 recipes for that specific query on a 296-recipe
household), but the CLI didn't expose that capability. This PR
turns the manual investigation into a first-class flag.

How

  • New mealie.SearchRecipes(ctx, query, opts) method in
    internal/mealie/search.go that:
    1. pulls the full recipe list (no fuzzy ?search= hint)
    2. post-filters on the field(s) requested
    3. when ingredients are in scope, fetches each recipe's full
      detail concurrently (8 workers by default, bounded by a
      semaphore)
  • New --in flag in the recipe search CLI, default both
  • New RecipeIngredient type added to internal/mealie/types.go
    so recipe get actually decodes and returns the ingredient list
    (it was previously dropped silently)
  • Matching is case- and diacritic-insensitive: bœuf matches
    boeuf, BŒUF, Boeuf, crème matches creme. Uses NFKD +
    a manual mapping for the œ ligature (Go's norm.NFKD in
    golang.org/x/text v0.38.0 doesn't decompose it — verified
    with a go run debug).
  • Bump version 0.1.0 → 0.2.0
  • Update the in-repo skills/mealie-cli/SKILL.md with the new
    flag, four example invocations, and the rationale for the change

Verification

# Default — match in title OR ingredients
mealie-cli recipe search "boeuf haché"
# → 16 results, all real ground-beef recipes

mealie-cli recipe search "boeuf haché" --in ingredients
# → 16 results, all verified to have 'boeuf haché' as an ingredient line

mealie-cli recipe search "boeuf haché" --in title
# → 13 results, all with 'boeuf' somewhere in the title
#   (mixes ground beef with braised / bourguignon / effiloché;
#    user can narrow down further with --in ingredients or jq)

# Accent folding works in both directions
mealie-cli recipe search "bœuf" --in ingredients
# → same 16 results as "boeuf"

# Invalid value
mealie-cli recipe search "boeuf" --in trucs
# → Error: mealie: invalid --in value "trucs" (want title|ingredients|both)

Tests: 23 new test cases

  • 8 table-driven for ParseSearchField
  • 8 for fold (covers œ, BŒUF, bœuf, crème, déjà vu,
    fallback for unknown chars)
  • 10 for matchInText (case + accent insensitivity, word
    boundaries, empty needles, etc.)
  • 4 for matchTitle
  • 7 for matchIngredients (note + display fallback)
  • 5 integration tests using a fakeTransport (httptest-style
    fake) that asserts the right slugs come back for each
    SearchField, that accent folding works end-to-end, and that
    Limit is respected

Performance: --in title makes 1 paginated list call and no detail
calls. --in ingredients and --in both make 1 list call + 1 GET
per recipe, bounded by 8 concurrent fetches. For 296 recipes, the
boeuf-haché query returns in ~12s.

Diffstat

 flake.nix                               |   2 +-
 go.mod                                  |   3 +-
 go.sum                                  |   2 +
 internal/cli/recipe.go                  |  36 +++-
 internal/cli/version_vars.go            |   2 +-
 internal/mealie/search.go               | 251 +++++++++++++++++++++
 internal/mealie/search_fake_test.go     |  80 ++++++
 internal/mealie/search_test.go          | 278 ++++++++++++++++++++++++++
 internal/mealie/search_testdata_test.go |  17 ++
 internal/mealie/types.go                | 103 ++++++----
 skills/mealie-cli/SKILL.md              |  38 +++-
 11 files changed, 756 insertions(+), 56 deletions(-)

Caveats

  • Breaking change in output semantics for recipe search: the
    default behaviour (now --in both) is different from before
    (which used Mealie's fuzzy ?search=). The new behaviour is
    strictly more correct (no false positives) and is what users
    actually want, but anyone with shell scripts relying on
    recipe search returning ~100 fuzzy results will see a change.
    The previous behaviour can still be approximated with
    recipe list --search <query>, which still uses Mealie's fuzzy
    filter (though that has its own caveats — see the skill).
  • No new dependency on the golang.org/x/text family for
    accented-char folding. The repo now has one indirect dependency
    (golang.org/x/text v0.38.0) — small, well-maintained, no
    transitive bloat. Could be replaced by a 20-line pure-Go fold
    function if the dependency is unwanted, but the norm package
    does the right thing for the ~30 diacritics found in real recipe
    text and is the more conservative choice.
# What Add a `--in {title,ingredients,both}` flag to `mealie-cli recipe search`. Until now, `recipe search` was a thin pass-through to Mealie's `/api/recipes?search=…` endpoint, which is fuzzy and unreliable (it returns 100 results even when only a handful contain the query, and it doesn't search ingredient lines at all). The new flag makes the search deterministic and adds ingredient-level matching. # Why User case: "find me recipes that use ground beef" (recettes qui utilisent du boeuf haché). The previous `recipe search boeuf` returned 100 results, mostly unrelated. Going to the API and post-filtering on each recipe's `recipeIngredient[]` list gave the real answer (16 recipes for that specific query on a 296-recipe household), but the CLI didn't expose that capability. This PR turns the manual investigation into a first-class flag. # How - New `mealie.SearchRecipes(ctx, query, opts)` method in `internal/mealie/search.go` that: 1. pulls the full recipe list (no fuzzy `?search=` hint) 2. post-filters on the field(s) requested 3. when ingredients are in scope, fetches each recipe's full detail concurrently (8 workers by default, bounded by a semaphore) - New `--in` flag in the `recipe search` CLI, default `both` - New `RecipeIngredient` type added to `internal/mealie/types.go` so `recipe get` actually decodes and returns the ingredient list (it was previously dropped silently) - Matching is case- and diacritic-insensitive: `bœuf` matches `boeuf`, `BŒUF`, `Boeuf`, `crème` matches `creme`. Uses NFKD + a manual mapping for the `œ` ligature (Go's `norm.NFKD` in `golang.org/x/text v0.38.0` doesn't decompose it — verified with a `go run` debug). - Bump version 0.1.0 → 0.2.0 - Update the in-repo `skills/mealie-cli/SKILL.md` with the new flag, four example invocations, and the rationale for the change # Verification ```bash # Default — match in title OR ingredients mealie-cli recipe search "boeuf haché" # → 16 results, all real ground-beef recipes mealie-cli recipe search "boeuf haché" --in ingredients # → 16 results, all verified to have 'boeuf haché' as an ingredient line mealie-cli recipe search "boeuf haché" --in title # → 13 results, all with 'boeuf' somewhere in the title # (mixes ground beef with braised / bourguignon / effiloché; # user can narrow down further with --in ingredients or jq) # Accent folding works in both directions mealie-cli recipe search "bœuf" --in ingredients # → same 16 results as "boeuf" # Invalid value mealie-cli recipe search "boeuf" --in trucs # → Error: mealie: invalid --in value "trucs" (want title|ingredients|both) ``` Tests: 23 new test cases - 8 table-driven for `ParseSearchField` - 8 for `fold` (covers `œ`, `BŒUF`, `bœuf`, `crème`, `déjà vu`, fallback for unknown chars) - 10 for `matchInText` (case + accent insensitivity, word boundaries, empty needles, etc.) - 4 for `matchTitle` - 7 for `matchIngredients` (note + display fallback) - 5 integration tests using a `fakeTransport` (httptest-style fake) that asserts the right slugs come back for each `SearchField`, that accent folding works end-to-end, and that `Limit` is respected Performance: `--in title` makes 1 paginated list call and no detail calls. `--in ingredients` and `--in both` make 1 list call + 1 GET per recipe, bounded by 8 concurrent fetches. For 296 recipes, the boeuf-haché query returns in ~12s. # Diffstat ``` flake.nix | 2 +- go.mod | 3 +- go.sum | 2 + internal/cli/recipe.go | 36 +++- internal/cli/version_vars.go | 2 +- internal/mealie/search.go | 251 +++++++++++++++++++++ internal/mealie/search_fake_test.go | 80 ++++++ internal/mealie/search_test.go | 278 ++++++++++++++++++++++++++ internal/mealie/search_testdata_test.go | 17 ++ internal/mealie/types.go | 103 ++++++---- skills/mealie-cli/SKILL.md | 38 +++- 11 files changed, 756 insertions(+), 56 deletions(-) ``` # Caveats - Breaking change in output semantics for `recipe search`: the default behaviour (now `--in both`) is different from before (which used Mealie's fuzzy `?search=`). The new behaviour is strictly more correct (no false positives) and is what users actually want, but anyone with shell scripts relying on `recipe search` returning ~100 fuzzy results will see a change. The previous behaviour can still be approximated with `recipe list --search <query>`, which still uses Mealie's fuzzy filter (though that has its own caveats — see the skill). - No new dependency on the `golang.org/x/text` family for accented-char folding. The repo now has one indirect dependency (`golang.org/x/text v0.38.0`) — small, well-maintained, no transitive bloat. Could be replaced by a 20-line pure-Go fold function if the dependency is unwanted, but the `norm` package does the right thing for the ~30 diacritics found in real recipe text and is the more conservative choice.
Until now 'recipe search' was a thin pass-through to Mealie's fuzzy
?search= endpoint, which is unreliable: it returns 100 results even
when only a handful actually contain the query (e.g. searching
'boeuf' returns 'Aiglefin au curry et miel' just because the
description mentions boeuf). It also doesn't search ingredient
lines, so 'find me recipes that USE ground beef' wasn't directly
answerable from the CLI.

This commit adds a new SearchRecipes method that:
  1. pulls the full recipe list (predictable, no fuzzy filter)
  2. post-filters on the field(s) requested
  3. when ingredients are in scope, fetches each recipe's full
     detail concurrently (8 workers by default) to scan its
     recipeIngredient[] list

The CLI gains a --in flag (default 'both') with three values:
  title        — case- and diacritic-insensitive substring on Name
  ingredients  — same on recipeIngredient[].note / .display
  both         — title OR ingredients (the new default)

Matching is case- and diacritic-insensitive: 'bœuf' (with
circumflex) matches 'Boeuf' (without), 'BŒUF' (capital ligature)
matches 'boeuf', etc. The fold uses NFKD + a manual mapping for
the 'œ' ligature (which Go's norm.NFKD doesn't decompose in
golang.org/x/text v0.38.0).

Other changes:
  - Recipe struct gains RecipeIngredient []RecipeIngredient
    (previously not decoded, so 'recipe get' was dropping the
    ingredient list)
  - bump version 0.1.0 → 0.2.0
  - update in-repo skill with the new flag, the four example
    invocations, and the rationale (Mealie's fuzzy is unreliable)

Tests: 23 new test cases (8 table-driven for ParseSearchField,
8 for fold, 10 for matchInText, 4 for matchTitle, 7 for
matchIngredients, 5 integration tests using a fake RoundTripper).
All pass on a real Go 1.26.3 toolchain.

Live verified against mealie.polensky.me (v3.12.0, 296 recipes):
'recipe search "boeuf haché" --in ingredients' returns the same
16 ground-beef recipes the Python investigation found, in ~12s.
This pull request can be merged automatically.
You are not authorized to merge this pull request.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin agent/search-by-ingredient:agent/search-by-ingredient
git switch agent/search-by-ingredient

Merge

Merge the changes and update on Forgejo.

Warning: The "Autodetect manual merge" setting is not enabled for this repository, you will have to mark this pull request as manually merged afterwards.

git switch main
git merge --no-ff agent/search-by-ingredient
git switch agent/search-by-ingredient
git rebase main
git switch main
git merge --ff-only agent/search-by-ingredient
git switch agent/search-by-ingredient
git rebase main
git switch main
git merge --no-ff agent/search-by-ingredient
git switch main
git merge --squash agent/search-by-ingredient
git switch main
git merge --ff-only agent/search-by-ingredient
git switch main
git merge agent/search-by-ingredient
git push origin main
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Lauria/mealie-cli!1
No description provided.