Skip to content

Geographic and recent-history recommendation strategies

Status: Draft for review (revised 2026-09-23). This is an implementation plan, not an approved specification or a record of implemented behavior. The user clarified the geography sources and request context, then defined a Swimlane as a configured, ordered sequence of accumulating strategy/geography variations, with browsing history as the final resort. Before implementation, update the observable contract in specs/commerce-recommendation-service.md.

Implementation reconciliation — 2026-09-25

The text below is the historical proposal; its draft status is not changed by this documentation audit. The additive contract now exists in the linked specification, and current code implements GeoKey, source attribution, typed recent projections, geographic set keys, and contextual ordered Swimlane resolution. Read the as-built explanation for the current mechanism and linked tests.

One important proposal is not implemented as written: _build_geographic_sets in pipeline/run.py returns accumulated output at the 50,000-local-set limit instead of failing the run. The proposed full configurable partition/pair/publication budget policy is also not the current deployment contract. Consequently, the fail-on-cap statements below must not be read as shipped guarantees. Local recipes are sparse behavioral/category recipes; not every strategy/geographic-level pair accepted in configuration has a published local counterpart. Resolving these differences requires an explicit behavior decision and tests, not silently changing the draft's intent in a documentation pass.

Outcome and scope

Complete these seven strategies: recommendations_using_recently_viewed, recommendations_using_recently_bought, popular_viewed_same_category, popular_bought_same_category, frequent_bought_different_category, frequent_viewed_same_category, and frequent_viewed_different_category. Each has city, region, country, and global variations. The five category strategies already have global implementations in the working tree; this plan extends them without treating geographic variants as 28 unrelated algorithms. The two recent strategies use an authorized Shopper's own recent views or purchases as seeds, respectively, and retrieve candidates from published item-neighborhood evidence.

The generator also needs visit geographies, transaction geographies, and a reproducible hierarchy. Its existing maximum cart-item and maximum session-page settings remain independent of geographic assignment.

Proposed contracts

  1. Canonical geography. Introduce a validated, source-local GeoKey with normalized country, region, and city components and a level discriminator. A city key includes its region and country; a region key includes its country. Build the key from the existing geo_location hierarchy, not from an external geocoder. Apply one versioned Unicode normalization/case-fold/whitespace rule to source values and userContext values; do not guess aliases or merge ambiguous places. ISO 3166-1/2 codes can be used when the source provides them, but the given name fields do not justify fabricating those codes. Unknown or mismatched city/region values simply leave that variation empty for the Swimlane to continue. Reject impossible partial hierarchies (city without region/country, region without country). A source correction or normalization-version change requires retraining its affected snapshot.
  2. Source attribution. In the one consistent source read, project online views and online purchase lines through their visit's geo_location_id joined to geo_location for city, region, and country. Project offline purchase lines from the location supplied with their transaction, not a visit join. All canonical rows still carry and validate Data Source/Tracking/Catalog scope. One browsing session or order must resolve to one geography before it can contribute pair support; conflicting assignments fail the run rather than splitting one context across places. Missing geography contributes to global evidence only. Do not retain visit, order, or Shopper IDs in derived storage.
  3. Training. Reduce each bounded context once, contributing derived support to its city, enclosing region, country, and global buckets. An event contributes once to popularity at each applicable level. Reuse co-view/co-purchase and popularity scoring; apply category equality/inequality before top-k truncation. Local sets are sparse: materialize only levels/anchors/categories with eligible evidence, under explicit partition, pair, and publication budgets. For a geographic candidate to publish, require at least 10 distinct qualifying browsing sessions or orders supporting that candidate at that level; this is a conservative sparse-evidence/disclosure-reduction default, not a guarantee that 10 different people contributed. Suppress exact local support counts/scores from external responses, restrict contextual serving to an authorized Commerce Property caller, and assess differencing across levels and successive snapshots before release. Keep the global strategy semantics and score denominators unchanged. Do not combine scores from distinct geographic levels during training or serving.
  4. Snapshot storage. Add a geographic dimension to the set identity, e.g. (snapshot_id, strategy, anchor_key, geo_level, geo_key), with a migration and an index supporting exact level/key lookup. Keep the global key explicit. Extend the snapshot repository with a bounded multi-key lookup under the same published head/transaction, so the four levels cannot come from different snapshots. Publication validates complete strategy outputs, then atomically switches the serving head as today. A pre-migration snapshot can serve its global sets, without manufacturing geographic evidence.
  5. Swimlane configuration and serving. A Swimlane is an ordered list of {strategy, geographic_level, filters} steps and a configurable quota N (1–100; use 50 as the proposed new-Swimlane default while leaving existing GET limit=20 unchanged). A common template lists city, region, country, then global variations, but the order of all strategy/geography steps is configuration, not a hidden per-strategy fallback. Validate each step's anchor shape, level, strategy, filters, maximum step count, and quota at configuration load. For each step, append eligible candidates in that step's deterministic order, excluding only Items already selected and any explicit step filters; stop at N. Do not blend scores across steps. After all configured steps, use the authorized Shopper's bounded recent view history in newest-first order to fill remaining slots, again deduplicating and enforcing eligibility/anchor exclusion. If there is no authorized history or too few eligible Items, return a shorter list; do not read raw interactions. Include source strategy, geographic level, and Swimlane step on each item in provenance. A mixed-level/mixed-strategy response shares one snapshot head. For example, a 50-item Swimlane can list popular_viewed_same_category@city, popular_viewed_same_category@region, frequent_viewed_different_category@country, then popular_viewed_same_category@global; recent-view history is implicit and runs only if those four steps together return fewer than 50 distinct Items.
  6. Request transport. Add a POST JSON resolution endpoint accepting scope, Swimlane name (or ID), optional anchor, userContext.city/region/country, and optional quota override within configured bounds. POST here still only reads recommendations: it makes no state change. The body keeps location and Shopper context out of routinely logged URLs; mark the response Cache-Control: private, no-store and do not log the body. Require the existing property authorization and Shopper personalization authorization before profile access. Preserve the existing GET endpoints and their global behavior for compatibility. The caller's location selects aggregate geography only; it does not prove Shopper identity. A body can contain {"swimlane":"discovery","userContext": {"city":"Montréal","region":"Québec","country":"Canada"}}; the scope and authorization remain mandatory request inputs. An absent userContext makes local steps empty and permits configured global steps.
  7. Recent Shopper strategies. Maintain separate bounded ordered recent-view and recent-purchase projections; the current combined recent_item_ids cannot distinguish the two. Read them only after existing Shopper authorization and withdrawal checks. For each seed, retrieve its published geographic co-view or co-purchase neighborhood. Combine bounded seeds with a documented, deterministic recency weight, and de-duplicate by candidate Item. Do not globally exclude purchased seed Items: exclude_recent_purchases is an explicit per-step/Swimlane filter, because a replenishment lane may want repeat purchases. Always exclude an anchored request's own anchor and Items already emitted by earlier steps. Keep profile state scoped to the Commerce Property; no source read or synchronous training in serving. Backfill/rebuild typed projections from retained authorized interaction data where available; otherwise start typed recents empty, without guessing from the combined list. Preserve the existing combined projection for current personalization until a separately verified migration removes it.
  8. Generator. Add a seeded, bounded hierarchy with distinct same-name cities in different parents. Materialize geo_location, visits referring to geo_location_id, and offline transaction location fields in SQLite and PostgreSQL. Generate online view/purchase rows through visits and offline purchase rows through transaction location. Update canonical SQL, dataset schema/manifest versions, table digests, append compatibility, sample config, and generator documentation together. Never emit raw Shopper/visit/order IDs into evidence artifacts. Synthetic proportions should include sparse cities, missing geography, and cross-region cases without violating row budgets.

Modules and boundaries

source owns validation and canonical projection; pipeline owns geographic reduction and candidate ranking; storage owns the snapshot key/migration; serving owns ordered Swimlane composition; personalization owns typed seed selection/authorization; synthetic owns reproducible source fixtures. Share GeoKey and the strategy's evidence/category rule across these boundaries, but do not let the request adapter, simulator, or source-specific SQL dictate ranking logic. serving_limits remains package-independent.

To control cardinality, add configured maximum distinct geographic partitions and derived candidate bytes/rows per run. Count and fail before exceeding them; never silently drop an arbitrary city that could change published results. Low-support geography is suppressed before publication. The ten-context floor applies per published geographic candidate, not merely to the overall city: ten unrelated visits in a city cannot justify a one-visit candidate. This is a starting control to evaluate against coverage and disclosure risk; multiple contexts may belong to one person, and overlapping levels/time snapshots can still leak information. A failed privacy-risk review keeps geographic output disabled until stronger controls are chosen. Metrics report aggregate bucket counts and step fill, not exact city names or raw identifiers.

Acceptance tests (independent observable oracles)

Requirement / risk Test oracle and red signal
Source attribution Visit A/B rows resolve through geo_location; offline transaction location wins even without a visit. A wrong region or join fails.
Context integrity A mixed-location session/order rejects publication; repeated lines/events count a context once for pair support.
Hierarchy and isolation Same-named cities in different regions stay distinct; region contains its cities exactly once, country its regions, and no Data Source/Tracking/Catalog evidence leaks.
Seven strategy semantics For each strategy, fixture rankings distinguish view from purchase, popularity from pairs, same from different category, and typed recent seeds; missing category does not match.
Swimlane fill A configured city → region → country → global sequence accumulates to quota without duplicates; reordering steps changes result order; each entry reports its actual strategy, step, and level.
History last resort Only after configured steps exhaust does authorized recent-view history append eligible distinct Items; absent/withdrawn history returns a short result, not a fabricated fill.
Profile safeguards Unauthorized/withdrawn Shopper yields no personalized history, view-only history cannot seed bought, cross-property history cannot seed either, and history caps hold.
Snapshot integrity One request reads one head; failed geographic training leaves old head intact; an old global-only head has a defined global fallback.
Generator Same seed/config yields identical table digests on SQLite/PostgreSQL; visit join and offline location are correct; append and max cart/pages bounds still hold.
Scale/privacy Geography cap fails safely; a candidate with 9 qualifying contexts is absent and one with 10 is eligible; public responses do not expose exact local support/score; no raw IDs or exact locality labels appear in logs/metrics/evidence.

Use fixed fixture scores and expected IDs as the oracle, not a second call to the same ranking function. Add unit tests for GeoKey, reduction, typed seed fusion, and Swimlane composition; contract tests for source SQL/request/response; integration tests for migration, publication, generator, and end-to-end serving. Compare candidate coverage/holdout metrics with existing global baselines before claiming relevance gains.

Implementation sequence and verification

  1. Ratify the observable spec: strategy names, Swimlane config/quota and history semantics, POST/request shape, geographic provenance, support floor, and old-GET behavior. Add executable contract examples first.
  2. Implement GeoKey, source projections/validation, and synthetic geography; test both online join and offline direct location across SQLite/PostgreSQL.
  3. Extend the bounded reducer and sparse geographic snapshot schema; migrate storage and test atomic publication, old-head fallback, and caps.
  4. Implement shared configured Swimlane composition for the five category strategies, then typed Shopper projections, the two recent strategies, and the history-last-resort step. Reuse the same composition tests across all strategy/geography variations.
  5. Update technical, personalization, simulation, and operations docs; run make test-focused per touched layer, then make test-static, make test-unit, make test-contract, make test-integration, make migration-check, make smoke, and make verify. Run make test-postgres only with an explicitly disposable test database; report it as not run otherwise. Measure geographic partition/cardinality growth and serving latency on a bounded synthetic workload.

Resolved recommendations and release gate

  • Use 10 distinct supporting contexts per candidate and geographic level as the initial publish threshold; treat it as evidence/disclosure reduction, not anonymization. The contextual endpoint requires authorized property access, omits exact local support/score, and is gated on a documented disclosure-risk review (especially level and snapshot differencing). This is based on ICO's small-area guidance, NIST's warning about re-identification, and ONS's differencing analysis, not on a claim that any fixed count makes output anonymous.
  • Use source-local hierarchical names with a versioned Unicode normalization rule, because the supplied source has names and a visit location ID, not guaranteed ISO or GeoNames IDs. ISO 3166 covers countries/subdivisions, not universal cities; Unicode normalization resolves equivalent encodings but not city aliases. The generator emits its own stable hierarchical IDs and names; no network geocoding occurs during training or serving.
  • Use POST JSON for contextual Swimlane resolution and keep legacy GET global. RFC 9110 §9.3.1 warns that sensitive query data may be inappropriate in URIs and recommends POST content where caching has no benefit. no-store, body-redaction, and authorization remain necessary; POST alone is not a privacy control.
  • Do not impose a universal recent-purchase exclusion. An explicit Swimlane step filter decides whether previously purchased Items are eligible; browsing history remains the final filler after configured model variations.

No external service or licensing dependency is introduced for geography.

Research basis: strategy evidence, including item-neighborhood retrieval, session recency, hierarchical aggregation, and location privacy. These references motivate the architecture; the thresholds and API shape above are explicit service proposals, not claims proven by them.