One Food Search Page, Four Different Counts
Fresh Open Food Facts search work shows why food and recipe APIs should separate total matches, count exactness, returned hits, and renderable cards while preserving one canonical filter plan.
“24 results” is not one fact
A food search page can truthfully say that a query matched 18,000 records, returned 24 hits, and displayed 22 cards. It can also be wrong about all three while still receiving a successful API response.
Fresh work in Open Food Facts Explorer makes this distinction unusually visible. A facet-filtering change merged August 28 added a 35-field catalog, include and exclude controls, URL hydration, field aliases, and query round-trip tests. A separate result-summary proposal opened August 28 displays the total, the number of cards actually shown, search time, and different wording when the total is not exact. The latter remains open at publication, so it is evidence of an implementation decision, not a shipped guarantee.
The thesis: recipe and food-search APIs should return an observable query execution, not merely an array—one canonical predicate plus separate counts for total matches, page hits, renderable items, and displayed items, with certainty attached to the total. Without that separation, facets can look correct while pagination, result copy, analytics, and customer decisions describe different populations.
Source map and the repeated angle to avoid
Fresh primary evidence from the last seven days:
| Source | Status on August 28, 2026 | What it contributes |
|---|---|---|
| Open Food Facts Explorer facet-filtering pull request | Merged August 28 | Shows catalog-to-index field aliases, include/exclude state, Lucene serialization and parsing, special-character tests, and fixes that make opposing selections mutually exclusive. |
| Open Food Facts Explorer result-summary pull request | Open; created August 28 | Separates total count from rendered-card count, respects is_count_exact, reports elapsed search time, and derives displayed count from the same filtered list used by the card loop. |
The ten most recent posts here already cover ingredient separator parsing, tree export contracts, nutrition validation gates, image-derived evidence, recall identities, multi-jurisdiction nutrition, food-table migrations, policy engines, lot-aware identifiers, and meal-to-cart contracts. Older search posts cover ingestion-time normalization and stable facet endpoints. The repeated angle to avoid is “normalize recipe fields before indexing” or “treat facets as contracts.”
This article starts one layer later: after fields and facets exist, how does a client prove which predicate ran and what each count actually counts?
Four counts, four different questions
A production search response needs to distinguish at least these values:
| Count | Question answered | Typical source |
|---|---|---|
| Total matches | How many records satisfy the applied predicate? | Search engine total |
| Page hits | How many records did this request return before client eligibility checks? | Response array length |
| Renderable items | How many returned records can become valid cards or API objects? | Product validation and presentation policy |
| Displayed items | How many cards did this interface actually render? | Final UI list after caps, windowing, or presentation rules |
Open Food Facts Explorer’s proposed summary makes the last distinction concrete. Its visible list excludes products without a product code, and the displayed number is derived from that same list. Therefore a page can receive 24 hits but show fewer than 24 cards. Reporting the requested page size—or even raw hits.length—would misdescribe the interface.
Recipe products have equivalent gaps. A hit may be unrenderable because it lacks a stable recipe ID, localized title, licensed image, complete instruction steps, tenant entitlement, or required safety metadata. A grocery result may lack a canonical ingredient or purchasable SKU. A nutrition result may fail the product’s minimum completeness threshold.
Do not erase those records silently. Return the counts and reasons separately. In a simple page, renderable and displayed counts may be equal. They diverge when a client applies a visible-item cap, virtualized window, experiment, or final presentation rule.
Exactness belongs beside the number
Large search engines may stop counting after a threshold or return an approximate total to control latency. The proposed Open Food Facts copy uses “More than …” when is_count_exact is false instead of presenting the value as an exact fact.
That wording decision exposes an API-design rule: a bare integer is insufficient when its relation to the real population can vary. Prefer a structure such as:
{
"total": {
"value": 10000,
"relation": "lower_bound"
},
"pageHits": 24,
"renderableItems": 22,
"displayedItems": 22
}
relation can be exact, lower_bound, or estimate. If an estimate has a documented error margin, include it. Do not infer exactness from a threshold such as exactly 10,000; thresholds change, and different query plans can produce different certainty.
Pagination must follow the same rule. An exact pageCount cannot be derived from a lower-bound total. A client can instead use a continuation token, hasNextPage, or qualified copy such as “at least 417 pages.” Product analytics should also record the relation. Otherwise a search infrastructure optimization can appear as a sudden collapse in catalog coverage.
Preserve one canonical predicate across three representations
Facet-heavy food search commonly has three versions of the query:
- controls in the interface;
- a shareable URL or client request;
- the backend search expression.
The merged facet work shows why the transitions are real engineering surfaces. It maps public facet keys such as packaging_shapes to index fields such as packagings.shape, serializes included and excluded values, parses the expression back into controls, escapes quotes and backslashes, and tests values containing words such as “AND” and “OR.” It also added fixes so one value does not remain both included and excluded.
A recipe query makes the stakes clearer:
{
"text": "quick noodles",
"filters": {
"all": [
{"field": "dietIds", "op": "contains", "value": "vegan"},
{"field": "allergenIds", "op": "not_contains", "value": "peanut"},
{"field": "totalTimeMinutes", "op": "lte", "value": 30}
]
}
}
Treat this typed form as the source of truth. A Lucene string, query parameter, SQL predicate, or provider-specific filter should be a compiled representation—not the primary product model.
That separation allows the API to reject unknown fields, validate operator compatibility, resolve taxonomy aliases, and publish a canonical form. It also avoids relying on display labels such as “peanut-free” as identifiers. Most importantly, the server can return the effective predicate it actually applied. A user-visible filter chip is not proof that the backend received it.
Return a query receipt
A useful search response can expose execution truth without leaking an internal query language:
{
"query": {
"requested": {
"text": "quick noodles",
"filters": ["diet:vegan", "exclude-allergen:peanut", "time-lte:30"]
},
"applied": {
"text": "quick noodles",
"canonicalFilterIds": ["diet:vegan", "allergen:peanut", "time:30"]
},
"status": "fully_applied",
"snapshot": "catalog-2026-08-28"
},
"results": {
"total": {"value": 10000, "relation": "lower_bound"},
"pageHits": 24,
"renderableItems": 22,
"displayedItems": 22,
"dropped": [
{"reason": "missing_required_recipe_id", "count": 2}
]
},
"timing": {
"searchMs": 183
}
}
This is a query receipt. It answers whether every requested filter was understood, which canonical IDs were used, which catalog snapshot supplied the result, and why records disappeared between the engine and interface.
A partial application should not masquerade as success. If an API cannot support a requested nutrient range or allergen exclusion, fail with a stable error or return status: partially_applied plus explicit rejected filters. Silently dropping a narrowing constraint can turn “recipes without peanuts” into an unsafe broad search.
Failure modes to test before launch
Count and predicate errors often survive unit tests because every individual component looks plausible. Test the composed behavior:
| Case | Required invariant |
|---|---|
| Included and excluded value are identical | Canonicalization rejects the query or deterministically keeps one state. |
Value contains quotes, backslashes, AND, or OR |
UI → request → parser → UI round-trip is lossless. |
| Taxonomy label changes | Stable filter ID and results remain unchanged; only display text changes. |
| Total is a lower bound | Copy and pagination never present it as exact. |
| Two hits fail card requirements | pageHits remains two higher than renderableItems, with drop reasons. |
| Personalization reorders results | Predicate and total remain stable unless personalization also filters. |
| Client changes a filter during navigation | Returned receipt identifies the applied state, preventing stale chips from claiming otherwise. |
| Backend times out after partial work | Response declares timeout/partial status; cached totals are not mixed with fresh hits. |
| Tenant or region policy removes recipes | Eligibility count is distinguished from engine count. |
| No matches | The product returns zero for the applied predicate, never an unfiltered fallback. |
Also compare list, facet, export, recommendation, and analytics endpoints. If they cannot cite the same canonical predicate and snapshot, their totals are not safely comparable.
Practical decisions for recipe API teams
Before shipping advanced food search, decide:
- which representation is canonical: typed filter AST, URL, or engine string;
- whether unknown filters fail closed;
- how taxonomy labels map to stable ingredient, diet, cuisine, and allergen IDs;
- whether totals are exact, bounded, or estimated;
- what makes a recipe renderable or commercially eligible;
- whether missing data means false, unknown, or excluded;
- how continuation works when an exact page count is unavailable;
- which drop reasons are safe to expose to clients;
- how query snapshots survive index and taxonomy deployments;
- whether search latency measures only the engine or the full enrichment path.
Structured recipe data still matters: canonical ingredients, explicit allergens, normalized times, nutrition quantities, and provenance make reliable filters possible. But the search contract is not complete until clients can observe how those fields became a predicate and how that predicate became each count.
For developers and technical buyers evaluating a recipe API, ask for a response that explains itself. The useful question is no longer just “Can I filter by ingredient and diet?” It is “Can I prove which filters ran, whether the count is exact, and why the number of usable recipes differs from the number matched?”
Sources
- Open Food Facts Explorer, feat(search): improve facet filtering, merged August 28, 2026.
- Open Food Facts Explorer, feat(search): summarize search result counts, opened August 28, 2026; open at publication.
- Background: Open Food Facts Search-a-licious, user tutorial.
Start Building
One consistent schema on every response. Get a free key and ship in minutes.