Skip to content

Search products across five open databases, and fix the broken OFF search (epic #43, backend) - #66

Merged
untraceablez merged 2 commits into
mainfrom
feat/multi-source-product-search
Jul 27, 2026
Merged

Search products across five open databases, and fix the broken OFF search (epic #43, backend)#66
untraceablez merged 2 commits into
mainfrom
feat/multi-source-product-search

Conversation

@untraceablez

Copy link
Copy Markdown
Owner

Part of #43. Backend half; the frontend PR that renders the grouped results follows.

The bug

The product name search shipped in #59 called the Open Food Facts v2 API with a search_terms parameter. v2 has no such parameter — it was silently ignored, so the endpoint returned an arbitrary slice of the 4.6M-product database:

GET /api/v1/barcode/search?q=peanut+butter
→ Fromage Blanc Nature, Sidi Ali (bottled water), Perly
→ or {"results":[]}   (intermittently)

The existing tests didn't catch it because they mock the transport — the payload shape was right, only the request was wrong. There's now a regression test asserting the OFF query goes to Search-a-licious with q, and that search_terms never comes back.

Open Food Facts is now queried through Search-a-licious (search.openfoodfacts.org), which is relevance-ranked and returns the image URLs the suggestion list needs. Its legacy cgi/search.pl endpoint is currently serving "Page temporarily unavailable" under load, so it isn't used as a fallback.

Multi-source search

Search fans out concurrently to five sources and groups the hits:

Source Endpoint Adds
Open Food Facts Search-a-licious packaged food
Open Beauty Facts cgi/search.pl toiletries, cosmetics
Open Products Facts cgi/search.pl general household goods
Open Pet Food Facts cgi/search.pl pet food
USDA FoodData Central FDC API generic/whole foods

The lower-traffic OFF siblings have no Search-a-licious index, but their CGI search works fine. USDA covers the generic/whole foods (bananas, raw) that the barcode-keyed databases don't carry.

Each group carries its own "see more" link. Sources returning nothing are omitted, and a source that times out, rate-limits or dies degrades to an empty group rather than failing the whole search.

Live output for q=peanut+butter:

== Open Food Facts       Peanut Butter (Peanut Butter & Co) …
== Open Beauty Facts     Fwee Lip Balm Peanut Butter Carmel …
== Open Products Facts   Chocolate Peanut Butter (MYFITNESS) …
== Open Pet Food Facts   Peamutt Butter, Peanut Butter & Banana Cookies (Wagg)
== USDA FoodData Central Peanut Butter, Peanut Butter (Wild Oats) …

API changes

  • GET /barcode/search gains groups: [{source, label, results, search_url}]. Suggestions now carry source, source_label and id alongside barcode.
  • New GET /barcode/product?source=&id= — selecting a suggestion needs the source, because USDA foods are keyed by FDC id rather than barcode. Unknown sources 400 with the supported list.
  • GET /barcode/{barcode} and the flat results/search_url fields are kept, so the currently deployed frontend keeps working until it moves to the grouped shape.

Config

USDA_FDC_API_KEY defaults to the rate-limited DEMO_KEY (~30 req/hr/IP); a free key comes from https://fdc.nal.usda.gov/api-key-signup. Setting it empty drops USDA from search entirely.

Testing

  • 417 passed — full backend suite, no regressions.
  • 100% line coverage on all three touched source files (barcode_service.py 172, product_sources.py 30, api/v1/barcode.py 26).
  • Verified live against all five real APIs, plus source-aware lookup for off / obf / opff / usda, the 400 on an unknown source, and the legacy /barcode/{barcode} route.

🤖 Generated with Claude Code

Fixes the product name search and extends it beyond Open Food Facts.

The shipped search called the Open Food Facts **v2** API with a
`search_terms` parameter. v2 has no such parameter, so it was silently
ignored and the endpoint returned an arbitrary slice of the 4.6M-product
database — searching "peanut butter" returned Fromage Blanc and bottled
water. Open Food Facts is now queried through Search-a-licious
(search.openfoodfacts.org), which is relevance-ranked and returns the
image URLs the suggestion list needs. Its legacy cgi/search.pl endpoint
is frequently unavailable under load, so it is not used as a fallback.

Search now fans out concurrently to five sources, grouped by source:

  - Open Food Facts       (Search-a-licious)
  - Open Beauty Facts     (cgi/search.pl)
  - Open Products Facts   (cgi/search.pl)
  - Open Pet Food Facts   (cgi/search.pl)
  - USDA FoodData Central (FDC API)

The lower-traffic OFF siblings have no Search-a-licious index but their
CGI search works fine. USDA covers the generic/whole foods ("bananas,
raw") that the barcode-keyed databases do not carry.

Each group carries its own "see more" link. Sources returning nothing are
omitted, and a source that times out, rate-limits or dies degrades to an
empty group rather than failing the whole search.

Because USDA foods are keyed by FDC id rather than barcode, selecting a
suggestion now needs the source: GET /barcode/product?source=&id=.
GET /barcode/{barcode} and the flat results/search_url fields are kept so
the deployed frontend keeps working until it moves to the grouped shape.

USDA needs a free API key (USDA_FDC_API_KEY); it defaults to the
rate-limited DEMO_KEY, and setting it empty drops USDA from the search.

Covers all three touched files at 100%, including a regression test
asserting the OFF query goes to Search-a-licious with `q` and that
`search_terms` never comes back.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Sonar gate failed the build on two cognitive-complexity violations:

  _parse_usda_product   16 -> allowed 15
  _parse_product_data   31 -> allowed 15

_parse_product_data's complexity is long-standing -- its nutriment block
was 19 hand-written three-way `or` chains -- but the source parameter
added in the previous commit touched the signature, so Clean-as-You-Code
counts the whole function as new and the zero-new-violations condition
fails.

Both nutrient blocks become table-driven: _OFF_NUTRIMENT_MAP pairs each
Open Food Facts prefix with our key and the helper does the
serving -> 100g -> bare fallback in a loop, and the USDA nutrient loop
and serving-size join move to their own helpers.

Verified behaviour-identical rather than assumed: a differential harness
ran the old inline block and the new helper over four real Open Food
Facts products (up to 13 populated keys) plus edge cases for absent
nutriments, empty nutriments, serving-beats-100g, bare-key fallback, and
the quirk where a 0 value is falsy and falls through to the next
candidate. Zero mismatches, and key ordering is preserved.

Coverage stays at 100%; 417 backend tests pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant